mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
The embedded QQ Music API sidecar could bind a different port than the one the client base URL (getQQMusicBaseUrl) targets. The upstream @sansenjian/qq-music-api package derives its default port from process.env.PORT (falling back to 3200) and, in some historical versions, auto-started that server as an import side effect. When an old build listened on 3300 while the client requested 3200 (issue #122), fetching the QQ login QR failed with ECONNREFUSED on 127.0.0.1:3200, so the QR never showed and login / cookie persistence silently broke. Align process.env.PORT with the configured qqMusicApiPort for the duration of the import (restoring the previous value afterwards so nothing else in the process is affected), reuse an already-listening instance instead of racing a second listen, and log the port actually bound (read from the socket) so any mismatch is visible in the logs. - src/music/api-server.ts: PORT alignment + reuse-on-auto-start + bound-port log - src/music/api-server.test.ts: regression coverage that the sidecar follows qqMusicPort (not an injected PORT) and restores PORT afterwards - README.md: QQ login FAQ clarifies the sidecar and client share qqMusicApiPort and points stale-latest-image users (who saw 3300) at re-pulling the image Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
134 lines
4.9 KiB
TypeScript
134 lines
4.9 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { createApiServerManager, describeQqApiStartupError } from "./api-server.js";
|
|
import type { Logger } from "../logger.js";
|
|
|
|
// Record every listen() the QQ sidecar makes so we can assert it is always
|
|
// pinned to the configured port (regression coverage for issue #122).
|
|
const mockState = vi.hoisted(() => ({
|
|
listenCalls: [] as Array<{ port: number; host: string }>,
|
|
}));
|
|
|
|
vi.mock("@sansenjian/qq-music-api", () => {
|
|
const app = {
|
|
listen(port: number, host: string, cb?: () => void) {
|
|
mockState.listenCalls.push({ port, host });
|
|
const server = {
|
|
address: () => ({ port, address: host, family: "IPv4" as const }),
|
|
on() {
|
|
return server;
|
|
},
|
|
close(done?: () => void) {
|
|
done?.();
|
|
},
|
|
};
|
|
// Real net/Koa fire the listening callback on a later tick, after the
|
|
// caller has captured the returned server handle.
|
|
if (cb) setImmediate(cb);
|
|
return server;
|
|
},
|
|
};
|
|
return { default: app };
|
|
});
|
|
|
|
describe("describeQqApiStartupError", () => {
|
|
it("flags ERR_REQUIRE_ESM by error code with version-pin guidance", () => {
|
|
const hint = describeQqApiStartupError({ code: "ERR_REQUIRE_ESM", message: "..." });
|
|
expect(hint).toMatch(/ERR_REQUIRE_ESM/);
|
|
expect(hint).toMatch(/~2\.4\.0/);
|
|
expect(hint).toMatch(/~2\.2\.10/);
|
|
});
|
|
|
|
it("flags ERR_REQUIRE_ESM by message when the code is absent", () => {
|
|
const hint = describeQqApiStartupError(
|
|
new Error("require() of ES Module .../@sansenjian/qq-music-api/dist/index.js not supported")
|
|
);
|
|
expect(hint).toMatch(/incompatible @sansenjian\/qq-music-api/);
|
|
});
|
|
|
|
it("flags a Node engine mismatch with a Node-upgrade hint", () => {
|
|
const hint = describeQqApiStartupError(new Error("Unsupported engine: requires Node >=20.17"));
|
|
expect(hint).toMatch(/Node >=20\.17/);
|
|
expect(hint).toMatch(/~2\.2\.10/);
|
|
});
|
|
|
|
it("returns null for an unrelated startup error (falls back to the generic warning)", () => {
|
|
expect(describeQqApiStartupError(new Error("EADDRINUSE: port in use"))).toBeNull();
|
|
expect(describeQqApiStartupError(undefined)).toBeNull();
|
|
expect(describeQqApiStartupError(null)).toBeNull();
|
|
});
|
|
});
|
|
|
|
// Regression coverage for issue #122: the QQ Music API sidecar must listen on
|
|
// the same port the client base URL targets (config.qqMusicApiPort). A stale
|
|
// build once bound 3300 while the client requested 3200, silently breaking the
|
|
// QQ login QR / search flow with ECONNREFUSED on 127.0.0.1:3200.
|
|
describe("createApiServerManager — QQ sidecar port binding", () => {
|
|
const noopLogger = {
|
|
info() {},
|
|
warn() {},
|
|
error() {},
|
|
debug() {},
|
|
trace() {},
|
|
fatal() {},
|
|
} as unknown as Logger;
|
|
|
|
beforeEach(() => {
|
|
mockState.listenCalls = [];
|
|
});
|
|
|
|
it("listens on the configured qqMusicPort and exposes a matching base URL", async () => {
|
|
const port = 39217; // uncommon port to avoid clashing with a real instance
|
|
const manager = createApiServerManager(
|
|
{ neteasePort: 39218, qqMusicPort: port, neteaseEnabled: false, qqEnabled: true },
|
|
noopLogger
|
|
);
|
|
await manager.start();
|
|
manager.stop();
|
|
|
|
expect(manager.getQQMusicBaseUrl()).toBe(`http://127.0.0.1:${port}`);
|
|
expect(mockState.listenCalls).toEqual([{ port, host: "127.0.0.1" }]);
|
|
});
|
|
|
|
it("follows qqMusicPort — not an injected PORT — and restores PORT afterwards", async () => {
|
|
const port = 39219;
|
|
const previous = process.env.PORT;
|
|
// Simulate a hosting platform / compose file injecting a stray PORT that
|
|
// must NOT leak into the QQ sidecar's chosen port.
|
|
process.env.PORT = "39999";
|
|
const manager = createApiServerManager(
|
|
{ neteasePort: 39220, qqMusicPort: port, neteaseEnabled: false, qqEnabled: true },
|
|
noopLogger
|
|
);
|
|
try {
|
|
await manager.start();
|
|
// The sidecar follows qqMusicPort, never the injected PORT.
|
|
expect(mockState.listenCalls).toEqual([{ port, host: "127.0.0.1" }]);
|
|
// The injected PORT is restored so nothing else in the process is affected.
|
|
expect(process.env.PORT).toBe("39999");
|
|
} finally {
|
|
manager.stop();
|
|
if (previous === undefined) delete process.env.PORT;
|
|
else process.env.PORT = previous;
|
|
}
|
|
});
|
|
|
|
it("leaves an absent PORT env unset after importing the sidecar", async () => {
|
|
const port = 39221;
|
|
const previous = process.env.PORT;
|
|
delete process.env.PORT;
|
|
const manager = createApiServerManager(
|
|
{ neteasePort: 39222, qqMusicPort: port, neteaseEnabled: false, qqEnabled: true },
|
|
noopLogger
|
|
);
|
|
try {
|
|
await manager.start();
|
|
// Was unset before importing — must be unset again, no leaked override.
|
|
expect(process.env.PORT).toBeUndefined();
|
|
} finally {
|
|
manager.stop();
|
|
if (previous === undefined) delete process.env.PORT;
|
|
else process.env.PORT = previous;
|
|
}
|
|
});
|
|
});
|