Files

1790 lines
53 KiB
JavaScript

"use strict";
const fs = require("node:fs");
const test = require("node:test");
const assert = require("node:assert/strict");
const Module = require("node:module");
function loadPluginFresh() {
const modulePath = require.resolve("./music_server");
delete require.cache[modulePath];
return require("./music_server");
}
function loadPluginFromCode(code) {
const moduleObj = { exports: {} };
const wrapped = Function(
"'use strict'; return function(require,__musicfree_require,module,exports,console,env,URL,process){\n" +
code +
"\n}",
)();
wrapped(
require,
require,
moduleObj,
moduleObj.exports,
console,
undefined,
URL,
process,
);
return moduleObj.exports;
}
function clearRuntimeEnv() {
delete globalThis.env;
}
function createHttpClientStub(handler) {
const calls = [];
async function invoke(call) {
calls.push(call);
return handler(call, calls.length - 1);
}
return {
calls,
client: {
async get(url, config) {
return invoke({
url,
params: (config && config.params) || undefined,
});
},
async post(url, data) {
return invoke({
method: "post",
url,
data: data || undefined,
});
},
},
};
}
async function withPatchedModuleLoad(handler, run) {
const originalLoad = Module._load;
Module._load = function patchedLoad(request, parent, isMain) {
return handler(request, parent, isMain, originalLoad);
};
try {
return await run();
} finally {
Module._load = originalLoad;
}
}
test.afterEach(() => {
clearRuntimeEnv();
});
test("can require ./music_server", () => {
const plugin = loadPluginFresh();
assert.ok(plugin);
});
test("plugin.platform is Music_Server", () => {
const plugin = loadPluginFresh();
assert.equal(plugin.platform, "Music_Server");
});
test("plugin.supportedSearchType includes music, artist, and sheet", () => {
const plugin = loadPluginFresh();
assert.deepEqual(plugin.supportedSearchType, ["music", "artist", "sheet"]);
});
test("plugin.userVariables has only baseUrl and accessToken", () => {
const plugin = loadPluginFresh();
const keys = plugin.userVariables.map((item) => item.key);
assert.deepEqual(keys, ["baseUrl", "accessToken"]);
});
test("readConfigValue reads from env.getUserVariables", () => {
globalThis.env = {
getUserVariables: () => ({
baseUrl: "https://env-getter.example.com/",
accessToken: "getter-token",
}),
userVariables: {
baseUrl: "https://env-user.example.com/",
accessToken: "user-token",
},
};
const plugin = loadPluginFresh();
plugin.__clearTestState();
assert.equal(
plugin.readConfigValue("baseUrl"),
"https://env-getter.example.com/",
);
assert.equal(plugin.readConfigValue("accessToken"), "getter-token");
});
test("readConfigValue falls back to env.userVariables", () => {
globalThis.env = {
userVariables: {
baseUrl: "https://env-user.example.com/",
accessToken: "user-token",
},
};
const plugin = loadPluginFresh();
plugin.__clearTestState();
assert.equal(
plugin.readConfigValue("baseUrl"),
"https://env-user.example.com/",
);
assert.equal(plugin.readConfigValue("accessToken"), "user-token");
});
test("readConfigValue tolerates throwing env.userVariables getter", () => {
globalThis.env = {
get userVariables() {
throw new Error("broken getter");
},
};
const plugin = loadPluginFresh();
plugin.__clearTestState();
assert.equal(plugin.readConfigValue("baseUrl"), undefined);
assert.equal(plugin.readConfigValue("accessToken"), undefined);
});
test("readConfigValue uses testConfig before runtime env", () => {
globalThis.env = {
getUserVariables: () => ({
baseUrl: "https://env-getter.example.com/",
accessToken: "getter-token",
}),
};
const plugin = loadPluginFresh();
plugin.__clearTestState();
plugin.__setConfigForTests({
baseUrl: "https://test-config.example.com/",
accessToken: "test-token",
});
assert.equal(
plugin.readConfigValue("baseUrl"),
"https://test-config.example.com/",
);
assert.equal(plugin.readConfigValue("accessToken"), "test-token");
});
test("readRuntimeValue tolerates throwing runtimeValues getter", () => {
globalThis.env = {
get runtimeValues() {
throw new Error("broken runtime values");
},
};
const plugin = loadPluginFresh();
plugin.__clearTestState();
assert.equal(plugin.readRuntimeValue("runtimeClientId"), undefined);
assert.equal(plugin.readRuntimeValue("runtimeClientLabel"), undefined);
});
test("getClient prefers injected test client", async () => {
let axiosRequireCount = 0;
await withPatchedModuleLoad((request, parent, isMain, originalLoad) => {
if (request === "axios") {
axiosRequireCount += 1;
throw new Error("should not require axios for injected client");
}
return originalLoad(request, parent, isMain);
}, async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const injectedClient = { type: "injected" };
plugin.__setHttpClientForTests(injectedClient);
const client = plugin.getClient();
assert.equal(client, injectedClient);
assert.equal(axiosRequireCount, 0);
});
});
test("default client is cached and rebuilt when config changes", async () => {
let axiosCreateCalls = 0;
const createdConfigs = [];
await withPatchedModuleLoad((request, parent, isMain, originalLoad) => {
if (request === "axios") {
return {
create: (config) => {
axiosCreateCalls += 1;
createdConfigs.push(config);
return {
id: axiosCreateCalls,
config,
};
},
};
}
return originalLoad(request, parent, isMain);
}, async () => {
globalThis.env = {
userVariables: {
baseUrl: "https://music-server.example.com/",
accessToken: "token-a",
},
};
const plugin = loadPluginFresh();
plugin.__clearTestState();
const firstClient = plugin.getClient();
const secondClient = plugin.getClient();
assert.equal(firstClient, secondClient);
assert.equal(axiosCreateCalls, 1);
assert.equal(createdConfigs[0].baseURL, "https://music-server.example.com");
assert.equal(createdConfigs[0].headers.Authorization, "Bearer token-a");
globalThis.env.userVariables.accessToken = "token-b";
const thirdClient = plugin.getClient();
assert.notEqual(thirdClient, firstClient);
assert.equal(axiosCreateCalls, 2);
assert.equal(createdConfigs[1].headers.Authorization, "Bearer token-b");
});
});
test("default client sends Authorization and X-Music-Client-Id headers", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
plugin.__setConfigForTests({
baseUrl: "https://music-server.example.com",
accessToken: "token-a",
runtimeClientId: "client-a",
runtimeClientLabel: "Alice iPhone",
});
let createdConfig = null;
await withPatchedModuleLoad((request, parent, isMain, originalLoad) => {
if (request === "axios") {
return {
create: (config) => {
createdConfig = config;
return { get() {}, post() {} };
},
};
}
return originalLoad(request, parent, isMain);
}, async () => {
plugin.getClient();
});
assert.equal(createdConfig.headers.Authorization, "Bearer token-a");
assert.equal(createdConfig.headers["X-Music-Client-Id"], "client-a");
assert.equal(createdConfig.headers["X-Music-Client-Label"], "Alice iPhone");
});
test("default client strips duplicated Bearer prefix in accessToken", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
plugin.__setConfigForTests({
baseUrl: "https://music-server.example.com",
accessToken: "Bearer token-with-prefix",
});
let createdConfig = null;
await withPatchedModuleLoad((request, parent, isMain, originalLoad) => {
if (request === "axios") {
return {
create: (config) => {
createdConfig = config;
return { get() {}, post() {} };
},
};
}
return originalLoad(request, parent, isMain);
}, async () => {
plugin.getClient();
});
assert.equal(
createdConfig.headers.Authorization,
"Bearer token-with-prefix",
);
});
test("default client sanitizes plugin js url to server root baseURL", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
plugin.__setConfigForTests({
baseUrl: "http://64.83.43.123:18081/plugins/music_server.js",
accessToken: "token-a",
});
let createdConfig = null;
await withPatchedModuleLoad((request, parent, isMain, originalLoad) => {
if (request === "axios") {
return {
create: (config) => {
createdConfig = config;
return { get() {}, post() {} };
},
};
}
return originalLoad(request, parent, isMain);
}, async () => {
plugin.getClient();
});
assert.equal(createdConfig.baseURL, "http://64.83.43.123:18081");
});
test("default client falls back to srcUrl-derived baseURL when baseUrl is empty", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
plugin.__setConfigForTests({
baseUrl: "",
srcUrl: "http://64.83.43.123:18081/plugins/music_server.js",
accessToken: "token-a",
});
let createdConfig = null;
await withPatchedModuleLoad((request, parent, isMain, originalLoad) => {
if (request === "axios") {
return {
create: (config) => {
createdConfig = config;
return { get() {}, post() {} };
},
};
}
return originalLoad(request, parent, isMain);
}, async () => {
plugin.getClient();
});
assert.equal(createdConfig.baseURL, "http://64.83.43.123:18081");
});
test("default client prefers srcUrl-derived baseURL over stale configured baseUrl", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
plugin.__setConfigForTests({
baseUrl: "http://111.228.62.29:18081",
srcUrl: "http://64.83.43.123:18081/plugins/music_server.js",
accessToken: "token-a",
});
let createdConfig = null;
await withPatchedModuleLoad((request, parent, isMain, originalLoad) => {
if (request === "axios") {
return {
create: (config) => {
createdConfig = config;
return { get() {}, post() {} };
},
};
}
return originalLoad(request, parent, isMain);
}, async () => {
plugin.getClient();
});
assert.equal(createdConfig.baseURL, "http://64.83.43.123:18081");
});
test("distributed plugin uses substituted srcUrl as absolute request base", async () => {
const pluginSourcePath = require.resolve("./music_server");
const rawCode = fs.readFileSync(pluginSourcePath, "utf8");
const distributedCode = rawCode.replace(
/__MUSIC_SERVER_PLUGIN_SRC_URL__/g,
"http://64.83.43.123:18081/plugins/music_server.js",
);
const plugin = loadPluginFromCode(distributedCode);
const originalFetch = globalThis.fetch;
const fetchCalls = [];
globalThis.fetch = async (url, options) => {
fetchCalls.push({ url, options });
return {
ok: true,
status: 200,
text: async () =>
JSON.stringify({
pinned: [{ id: "all", title: "all" }],
data: [],
}),
};
};
try {
await withPatchedModuleLoad((request, parent, isMain, originalLoad) => {
if (request === "axios") {
throw new Error("axios unavailable");
}
return originalLoad(request, parent, isMain);
}, async () => {
const result = await plugin.getRecommendSheetTags();
assert.equal(result.pinned[0].id, "all");
});
} finally {
globalThis.fetch = originalFetch;
}
assert.equal(fetchCalls.length, 1);
assert.equal(
fetchCalls[0].url,
"http://64.83.43.123:18081/mf/v1/recommend/tags",
);
});
test("default client auto-prepends http scheme when baseUrl has no protocol", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
plugin.__setConfigForTests({
baseUrl: "64.83.43.123:18081",
accessToken: "token-a",
});
let createdConfig = null;
await withPatchedModuleLoad((request, parent, isMain, originalLoad) => {
if (request === "axios") {
return {
create: (config) => {
createdConfig = config;
return { get() {}, post() {} };
},
};
}
return originalLoad(request, parent, isMain);
}, async () => {
plugin.getClient();
});
assert.equal(createdConfig.baseURL, "http://64.83.43.123:18081");
});
test("getRecommendSheetTags falls back to fetch when axios is unavailable", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
plugin.__setConfigForTests({
baseUrl: "",
srcUrl: "http://64.83.43.123:18081/plugins/music_server.js",
accessToken: "",
});
const originalFetch = globalThis.fetch;
const fetchCalls = [];
globalThis.fetch = async (url, options) => {
fetchCalls.push({ url, options });
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
pinned: [{ id: "all", title: "all" }],
data: [],
}),
};
};
try {
await withPatchedModuleLoad((request, parent, isMain, originalLoad) => {
if (request === "axios") {
throw new Error("axios unavailable");
}
return originalLoad(request, parent, isMain);
}, async () => {
const result = await plugin.getRecommendSheetTags();
assert.equal(result.pinned[0].id, "all");
});
} finally {
globalThis.fetch = originalFetch;
}
assert.equal(fetchCalls.length, 1);
assert.equal(fetchCalls[0].url, "http://64.83.43.123:18081/mf/v1/recommend/tags");
});
test("default client generates stable fallback X-Music-Client-Id from accessToken", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
plugin.__setConfigForTests({
baseUrl: "http://64.83.43.123:18081",
accessToken: "msv1_token_for_fallback_id",
});
const createdConfigs = [];
await withPatchedModuleLoad((request, parent, isMain, originalLoad) => {
if (request === "axios") {
return {
create: (config) => {
createdConfigs.push(config);
return { get() {}, post() {} };
},
};
}
return originalLoad(request, parent, isMain);
}, async () => {
const first = plugin.getClient();
const second = plugin.getClient();
assert.equal(first, second);
});
assert.equal(createdConfigs.length, 1);
const fallbackClientId = createdConfigs[0].headers["X-Music-Client-Id"];
assert.ok(typeof fallbackClientId === "string");
assert.ok(fallbackClientId.startsWith("mf-fallback-"));
assert.ok(fallbackClientId.length > "mf-fallback-".length);
});
test("axios remains lazy-loaded", async () => {
let axiosRequireCount = 0;
await withPatchedModuleLoad((request, parent, isMain, originalLoad) => {
if (request === "axios") {
axiosRequireCount += 1;
return {
create: (config) => ({ config }),
};
}
return originalLoad(request, parent, isMain);
}, async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
assert.equal(axiosRequireCount, 0);
globalThis.env = {
userVariables: {
baseUrl: "https://lazy-load.example.com/",
accessToken: "lazy-token",
},
};
const client = plugin.getClient();
assert.ok(client);
assert.equal(axiosRequireCount, 1);
});
});
test("search(music) requests /mf/v1/search/songs and maps to musicItem", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/search/songs") {
return {
data: {
page: 1,
page_size: 20,
total: 100,
data: [
{
id: "song-1",
title: "Moon",
artist: "Artist A",
album: "Album A",
artwork: "cover-a",
duration: 245,
},
{
id: 2,
name: "Moon River",
artists: [{ name: "Artist B" }, { name: "Artist C" }],
album: { name: "Album B", artwork: "cover-b" },
duration: 180000,
},
],
},
};
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.search("moon", 1, "music");
assert.deepEqual(stub.calls, [
{
url: "/mf/v1/search/songs",
params: {
q: "moon",
page: 1,
page_size: 20,
},
},
]);
assert.deepEqual(result, {
isEnd: false,
data: [
{
id: "song-1",
title: "Moon",
artist: "Artist A",
album: "Album A",
artwork: "cover-a",
duration: 245,
},
{
id: "2",
title: "Moon River",
artist: "Artist B, Artist C",
album: "Album B",
artwork: "cover-b",
duration: 180,
},
],
});
});
test("search(artist) requests /mf/v1/search/artists and maps to artist items", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/search/artists") {
return {
data: {
isEnd: true,
data: [
{
id: "catalogsync:artist:1",
name: "Singer A",
avatar: "artist-cover",
description: "artist-desc",
worksNum: 12,
supportedArtistTabs: ["music"],
},
],
},
};
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.search("singer", 1, "artist");
assert.deepEqual(stub.calls, [
{
url: "/mf/v1/search/artists",
params: {
q: "singer",
page: 1,
page_size: 20,
},
},
]);
assert.deepEqual(result, {
isEnd: true,
data: [
{
id: "catalogsync:artist:1",
name: "Singer A",
avatar: "artist-cover",
description: "artist-desc",
worksNum: 12,
platform: "catalogsync",
supportedArtistTabs: ["music"],
},
],
});
});
test("search(sheet) requests /mf/v1/search/sheets and maps to sheet items", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/search/sheets") {
return {
data: {
isEnd: true,
data: [
{
id: "catalogsync:playlist:1",
title: "Playlist A",
coverImg: "sheet-cover",
description: "sheet-desc",
worksNum: 20,
playableSongCount: 18,
play_count: 99,
},
],
},
};
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.search("playlist", 1, "sheet");
assert.deepEqual(stub.calls, [
{
url: "/mf/v1/search/sheets",
params: {
q: "playlist",
page: 1,
page_size: 20,
},
},
]);
assert.deepEqual(result, {
isEnd: true,
data: [
{
id: "catalogsync:playlist:1",
title: "Playlist A",
description: "sheet-desc",
coverImg: "sheet-cover",
worksNum: 20,
playableWorksNum: 18,
play_count: 99,
},
],
});
});
test("recommend tag APIs hit /mf/v1/recommend/tags and /mf/v1/recommend/sheets", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/recommend/tags") {
return {
data: {
pinned: [{ id: "hot", title: "热门" }],
data: [
{
title: "语种",
data: [{ id: "cn", name: "华语" }],
},
],
},
};
}
if (call.url === "/mf/v1/recommend/sheets") {
return {
data: {
page: 1,
page_size: 60,
total: 61,
data: [
{
id: "catalogsync:playlist:18165",
name: "夜晚歌单",
creator: { nickname: "Editor" },
description: "desc",
coverImg: "sheet-cover",
trackCount: 88,
play_count: 9999,
},
],
},
};
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const tags = await plugin.getRecommendSheetTags();
const sheets = await plugin.getRecommendSheetsByTag({ id: "cn" }, 1);
assert.deepEqual(stub.calls, [
{ url: "/mf/v1/recommend/tags", params: undefined },
{
url: "/mf/v1/recommend/sheets",
params: {
tag: "cn",
page: 1,
page_size: 60,
},
},
]);
assert.deepEqual(tags, {
pinned: [{ id: "hot", title: "热门" }],
data: [
{
title: "语种",
data: [{ id: "cn", title: "华语" }],
},
],
});
assert.deepEqual(sheets, {
isEnd: false,
data: [
{
id: "catalogsync:playlist:18165",
title: "夜晚歌单",
artist: "Editor",
description: "desc",
coverImg: "sheet-cover",
worksNum: 88,
play_count: 9999,
},
],
});
});
test('recommend sheets treats the legacy "default" tag as all', async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/recommend/sheets") {
return {
data: {
data: [],
},
};
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const sheets = await plugin.getRecommendSheetsByTag(
{ id: "", title: "默认" },
1,
);
assert.deepEqual(stub.calls, [
{
url: "/mf/v1/recommend/sheets",
params: {
tag: "all",
page: 1,
page_size: 60,
},
},
]);
assert.deepEqual(sheets, {
isEnd: true,
data: [],
});
});
test("recommend sheets maps playableSongCount to playableWorksNum", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/recommend/sheets") {
return {
data: {
data: [
{
id: "catalogsync:playlist:10001",
name: "Mapped Sheet",
playableSongCount: 23,
},
],
},
};
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const sheets = await plugin.getRecommendSheetsByTag({ id: "cn" }, 1);
assert.deepEqual(sheets, {
isEnd: true,
data: [
{
id: "catalogsync:playlist:10001",
title: "Mapped Sheet",
playableWorksNum: 23,
},
],
});
});
test("recommend sheets keeps zero playable counts", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/recommend/sheets") {
return {
data: {
data: [
{
id: "catalogsync:playlist:10002",
name: "Zero Count Sheet",
playableSongCount: 0,
},
{
id: "catalogsync:playlist:10003",
name: "Zero Works Sheet",
playableWorksNum: 0,
},
],
},
};
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const sheets = await plugin.getRecommendSheetsByTag({ id: "cn" }, 1);
assert.deepEqual(sheets, {
isEnd: true,
data: [
{
id: "catalogsync:playlist:10002",
title: "Zero Count Sheet",
playableWorksNum: 0,
},
{
id: "catalogsync:playlist:10003",
title: "Zero Works Sheet",
playableWorksNum: 0,
},
],
});
});
test("getMusicSheetInfo page 1 requests detail then tracks and returns sheetItem + musicList", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/playlists/18165") {
return {
data: {
id: "catalogsync:playlist:18165",
name: "晚安歌单",
creator: { nickname: "DJ" },
description: "sleep songs",
coverImg: "playlist-cover",
trackCount: 100,
play_count: 321,
},
};
}
if (call.url === "/mf/v1/playlists/18165/tracks") {
return {
data: {
page: 1,
page_size: 60,
total: 61,
data: [
{
id: "m1",
name: "Moonlight",
artists: [{ name: "A" }],
album: { name: "Night", coverImg: "album-cover" },
duration: 200000,
},
],
},
};
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.getMusicSheetInfo(
{ id: "catalogsync:playlist:18165" },
1,
);
assert.deepEqual(stub.calls, [
{ url: "/mf/v1/playlists/18165", params: undefined },
{
url: "/mf/v1/playlists/18165/tracks",
params: {
page: 1,
page_size: 60,
},
},
]);
assert.deepEqual(result, {
isEnd: false,
sheetItem: {
id: "catalogsync:playlist:18165",
title: "晚安歌单",
artist: "DJ",
description: "sleep songs",
coverImg: "playlist-cover",
worksNum: 100,
play_count: 321,
},
musicList: [
{
id: "m1",
title: "Moonlight",
artist: "A",
album: "Night",
artwork: "album-cover",
duration: 200,
},
],
});
});
test("getMusicSheetInfo accepts server musicList payload shape", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/playlists/18165/tracks") {
return {
data: {
isEnd: true,
musicList: [
{
id: "m1",
title: "Moonlight",
artist: "A",
album: "Night",
artwork: "album-cover",
duration: 200,
},
],
},
};
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.getMusicSheetInfo(
{ id: "catalogsync:playlist:18165" },
2,
);
assert.deepEqual(stub.calls, [
{
url: "/mf/v1/playlists/18165/tracks",
params: {
page: 2,
page_size: 60,
},
},
]);
assert.deepEqual(result, {
isEnd: true,
musicList: [
{
id: "m1",
title: "Moonlight",
artist: "A",
album: "Night",
artwork: "album-cover",
duration: 200,
},
],
});
});
test("getTopLists returns server toplist groups", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/toplists") {
return {
data: {
data: [
{
title: "官方榜",
data: [
{
id: "catalogsync:toplist:kuwo_top_16",
name: "酷我热歌榜",
description: "每周更新",
coverImg: "top-cover",
},
],
},
],
},
};
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.getTopLists();
assert.deepEqual(stub.calls, [
{ url: "/mf/v1/toplists", params: undefined },
]);
assert.deepEqual(result, [
{
title: "官方榜",
data: [
{
id: "catalogsync:toplist:kuwo_top_16",
title: "酷我热歌榜",
description: "每周更新",
coverImg: "top-cover",
},
],
},
]);
});
test("getTopListDetail page 1 requests detail then tracks and returns topListItem + musicList", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/toplists/kuwo_top_16") {
return {
data: {
id: "catalogsync:toplist:kuwo_top_16",
name: "酷我热歌榜",
description: "top hits",
coverImg: "top-cover",
},
};
}
if (call.url === "/mf/v1/toplists/kuwo_top_16/tracks") {
return {
data: {
isEnd: true,
data: [
{
id: "tm1",
title: "Hot Song",
artist: "Singer X",
album: "Hot Album",
artwork: "art-x",
duration: 222,
},
],
},
};
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.getTopListDetail(
{ id: "catalogsync:toplist:kuwo_top_16" },
1,
);
assert.deepEqual(stub.calls, [
{ url: "/mf/v1/toplists/kuwo_top_16", params: undefined },
{
url: "/mf/v1/toplists/kuwo_top_16/tracks",
params: {
page: 1,
page_size: 60,
},
},
]);
assert.deepEqual(result, {
isEnd: true,
topListItem: {
id: "catalogsync:toplist:kuwo_top_16",
title: "酷我热歌榜",
description: "top hits",
coverImg: "top-cover",
},
musicList: [
{
id: "tm1",
title: "Hot Song",
artist: "Singer X",
album: "Hot Album",
artwork: "art-x",
duration: 222,
},
],
});
});
test("getTopListDetail accepts server musicList payload shape", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/toplists/kuwo_top_16/tracks") {
return {
data: {
isEnd: false,
musicList: [
{
id: "tm1",
title: "Hot Song",
artist: "Singer X",
album: "Hot Album",
artwork: "art-x",
duration: 222,
},
],
},
};
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.getTopListDetail(
{ id: "catalogsync:toplist:kuwo_top_16" },
2,
);
assert.deepEqual(stub.calls, [
{
url: "/mf/v1/toplists/kuwo_top_16/tracks",
params: {
page: 2,
page_size: 60,
},
},
]);
assert.deepEqual(result, {
isEnd: false,
topListItem: {
id: "catalogsync:toplist:kuwo_top_16",
},
musicList: [
{
id: "tm1",
title: "Hot Song",
artist: "Singer X",
album: "Hot Album",
artwork: "art-x",
duration: 222,
},
],
});
});
test("search(non-music) returns empty result without network request", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.search("moon", 1, "album");
assert.deepEqual(result, {
isEnd: true,
data: [],
});
assert.equal(stub.calls.length, 0);
});
test("search fallback isEnd uses raw list length instead of mapped length", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const fullPageRawList = Array.from({ length: 20 }, (_, index) => ({
id: `song-${index}`,
title: `Title-${index}`,
artist: "Artist",
album: "Album",
artwork: "artwork",
duration: 200,
}));
fullPageRawList[7] = {};
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/search/songs") {
return {
data: {
page: 1,
page_size: 20,
data: fullPageRawList,
},
};
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.search("moon", 1, "music");
assert.equal(result.data.length, 19);
assert.equal(result.isEnd, false);
});
test("getMusicSheetInfo page 1 still loads tracks when detail request fails", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/playlists/18165") {
throw new Error("detail failed");
}
if (call.url === "/mf/v1/playlists/18165/tracks") {
return {
data: {
data: [
{
id: "m-1",
title: "Track 1",
artist: "Singer",
album: "Album 1",
artwork: "cover-1",
duration: 123,
},
],
},
};
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.getMusicSheetInfo(
{
id: "catalogsync:playlist:18165",
title: "Fallback Sheet",
artist: "Fallback Artist",
},
1,
);
assert.deepEqual(stub.calls, [
{ url: "/mf/v1/playlists/18165", params: undefined },
{
url: "/mf/v1/playlists/18165/tracks",
params: {
page: 1,
page_size: 60,
},
},
]);
assert.deepEqual(result.sheetItem, {
id: "catalogsync:playlist:18165",
title: "Fallback Sheet",
artist: "Fallback Artist",
});
assert.deepEqual(result.musicList, [
{
id: "m-1",
title: "Track 1",
artist: "Singer",
album: "Album 1",
artwork: "cover-1",
duration: 123,
},
]);
});
test("getTopListDetail page 1 still loads tracks when detail request fails", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/toplists/kuwo_top_16") {
throw new Error("detail failed");
}
if (call.url === "/mf/v1/toplists/kuwo_top_16/tracks") {
return {
data: {
isEnd: true,
data: [
{
id: "t-1",
title: "Top Track",
artist: "Top Singer",
album: "Top Album",
artwork: "top-art",
duration: 200,
},
],
},
};
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.getTopListDetail(
{
id: "catalogsync:toplist:kuwo_top_16",
title: "Fallback TopList",
},
1,
);
assert.deepEqual(stub.calls, [
{ url: "/mf/v1/toplists/kuwo_top_16", params: undefined },
{
url: "/mf/v1/toplists/kuwo_top_16/tracks",
params: {
page: 1,
page_size: 60,
},
},
]);
assert.deepEqual(result.topListItem, {
id: "catalogsync:toplist:kuwo_top_16",
title: "Fallback TopList",
});
assert.deepEqual(result.musicList, [
{
id: "t-1",
title: "Top Track",
artist: "Top Singer",
album: "Top Album",
artwork: "top-art",
duration: 200,
},
]);
});
test("getMusicSheetInfo keeps detail metadata when tracks request fails", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/playlists/18165") {
return {
data: {
id: "catalogsync:playlist:18165",
name: "Detail Sheet",
creator: { nickname: "Detail Author" },
description: "detail description",
coverImg: "detail-cover",
trackCount: 100,
play_count: 998,
},
};
}
if (call.url === "/mf/v1/playlists/18165/tracks") {
throw new Error("tracks failed");
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.getMusicSheetInfo(
{
id: "catalogsync:playlist:18165",
title: "Fallback Sheet",
},
1,
);
assert.deepEqual(result, {
isEnd: true,
sheetItem: {
id: "catalogsync:playlist:18165",
title: "Detail Sheet",
artist: "Detail Author",
description: "detail description",
coverImg: "detail-cover",
worksNum: 100,
play_count: 998,
},
musicList: [],
});
});
test("getTopListDetail keeps detail metadata when tracks request fails", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/mf/v1/toplists/kuwo_top_16") {
return {
data: {
id: "catalogsync:toplist:kuwo_top_16",
name: "Detail TopList",
description: "detail top description",
coverImg: "detail-top-cover",
updateFrequency: "weekly",
},
};
}
if (call.url === "/mf/v1/toplists/kuwo_top_16/tracks") {
throw new Error("tracks failed");
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.getTopListDetail(
{
id: "catalogsync:toplist:kuwo_top_16",
title: "Fallback TopList",
},
1,
);
assert.deepEqual(result, {
isEnd: true,
topListItem: {
id: "catalogsync:toplist:kuwo_top_16",
title: "Detail TopList",
description: "detail top description",
coverImg: "detail-top-cover",
artist: "weekly",
},
musicList: [],
});
});
test("getMediaSource joins baseUrl when stream url is relative", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
plugin.__setConfigForTests({
baseUrl: "https://media.example.com/",
accessToken: "",
});
const stub = createHttpClientStub((call) => {
if (call.method === "post" && call.url === "/mf/v1/media/resolve") {
return {
data: {
stream: {
url: "/mf/v1/media/stream/token-1",
headers: {
Referer: "https://music.example.com",
},
},
selected_source: {
quality: "super",
},
},
};
}
throw new Error(`Unexpected request: ${call.method} ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.getMediaSource({ id: "song-1" }, "high");
assert.deepEqual(stub.calls, [
{
method: "post",
url: "/mf/v1/media/resolve",
data: {
song_id: "song-1",
quality: "high",
},
},
]);
assert.deepEqual(result, {
url: "https://media.example.com/mf/v1/media/stream/token-1",
headers: {
Referer: "https://music.example.com",
},
quality: "super",
});
});
test("getPluginStatus requests /auth/v1/token-status", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.url === "/auth/v1/token-status") {
return {
data: {
valid: true,
status: "active",
remainingDays: 89,
playableSongCount: 12345,
isCurrentClientBound: true,
},
};
}
throw new Error(`Unexpected request: ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.getPluginStatus();
assert.deepEqual(stub.calls, [{ url: "/auth/v1/token-status", params: undefined }]);
assert.deepEqual(result, {
valid: true,
status: "active",
remainingDays: 89,
playableSongCount: 12345,
isCurrentClientBound: true,
});
});
test("getMediaSource keeps absolute stream url", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
plugin.__setConfigForTests({
baseUrl: "https://media.example.com/",
accessToken: "",
});
const stub = createHttpClientStub((call) => {
if (call.method === "post" && call.url === "/mf/v1/media/resolve") {
return {
data: {
stream: {
url: "https://cdn.example.com/audio/song-2.flac",
},
},
};
}
throw new Error(`Unexpected request: ${call.method} ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.getMediaSource({ id: "song-2" }, "standard");
assert.deepEqual(result, {
url: "https://cdn.example.com/audio/song-2.flac",
headers: {},
quality: "standard",
});
});
test("getMediaSource resolves protocol-relative stream url using baseUrl protocol", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
plugin.__setConfigForTests({
baseUrl: "https://media.example.com",
accessToken: "",
});
const stub = createHttpClientStub((call) => {
if (call.method === "post" && call.url === "/mf/v1/media/resolve") {
return {
data: {
stream: {
url: "//cdn.example.com/audio/song-3.flac",
},
},
};
}
throw new Error(`Unexpected request: ${call.method} ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.getMediaSource({ id: "song-3" }, "high");
assert.deepEqual(result, {
url: "https://cdn.example.com/audio/song-3.flac",
headers: {},
quality: "high",
});
});
test("getMediaSource returns null when resolve request fails", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.method === "post" && call.url === "/mf/v1/media/resolve") {
throw new Error("resolve failed");
}
throw new Error(`Unexpected request: ${call.method} ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.getMediaSource({ id: "song-4" }, "standard");
assert.equal(result, null);
});
test("getMediaSource returns null when stream url is missing", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub((call) => {
if (call.method === "post" && call.url === "/mf/v1/media/resolve") {
return {
data: {
stream: {
headers: {
Cookie: "k=v",
},
},
},
};
}
throw new Error(`Unexpected request: ${call.method} ${call.url}`);
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.getMediaSource({ id: "song-5" }, "standard");
assert.equal(result, null);
});
test("browse methods fail closed on request errors", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const fallbackSheetItem = {
id: "catalogsync:playlist:18165",
title: "Fallback Sheet",
};
const fallbackTopListItem = {
id: "catalogsync:toplist:kuwo_top_16",
title: "Fallback TopList",
};
const stub = createHttpClientStub(() => {
throw new Error("network down");
});
plugin.__setHttpClientForTests(stub.client);
const searchResult = await plugin.search("moon", 1, "music");
const sheetsResult = await plugin.getRecommendSheetsByTag({ id: "hot" }, 1);
const sheetInfoResult = await plugin.getMusicSheetInfo(fallbackSheetItem, 1);
const topListsResult = await plugin.getTopLists();
const topListDetailResult = await plugin.getTopListDetail(fallbackTopListItem, 1);
assert.deepEqual(searchResult, {
isEnd: true,
data: [],
});
assert.deepEqual(sheetsResult, {
isEnd: true,
data: [],
});
assert.deepEqual(sheetInfoResult, {
isEnd: true,
sheetItem: fallbackSheetItem,
musicList: [],
});
assert.deepEqual(topListsResult, []);
assert.deepEqual(topListDetailResult, {
isEnd: true,
topListItem: fallbackTopListItem,
musicList: [],
});
});
test("getRecommendSheetTags fails closed on request errors", async () => {
const plugin = loadPluginFresh();
plugin.__clearTestState();
const stub = createHttpClientStub(() => {
throw new Error("network down");
});
plugin.__setHttpClientForTests(stub.client);
const result = await plugin.getRecommendSheetTags();
assert.deepEqual(result, {
pinned: [],
data: [],
});
});
test("netease_17000 compatibility shell re-exports music_server", () => {
const musicServerPath = require.resolve("./music_server");
const neteasePath = require.resolve("./netease_17000");
delete require.cache[musicServerPath];
delete require.cache[neteasePath];
const musicServer = require("./music_server");
const neteaseCompat = require("./netease_17000");
assert.equal(neteaseCompat, musicServer);
});