mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
fix(spotify): refresh Web API search provider creds on Settings save (no restart) [corner-case R2-4]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
2413a9a3a1
commit
9a32f3c602
3 files changed
+106
No files matched your search
@@ -405,6 +405,97 @@ describe("bot router /settings applies spotify creds to the live OAuth (I2)", ()
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// R2-4: saving Spotify creds in Settings must also refresh the Web API SEARCH
|
||||||
|
// provider (spotifyProvider.setCreds), not only the OAuth playback path. Without
|
||||||
|
// this, a fresh install (enabled defaults false) keeps empty search creds until a
|
||||||
|
// full process restart even after an admin enters Client ID + Secret.
|
||||||
|
describe("bot router /settings refreshes the Web API search provider creds (R2-4)", () => {
|
||||||
|
let botDb: BotDatabase;
|
||||||
|
let app: express.Express;
|
||||||
|
let cookie: string;
|
||||||
|
let config: BotConfig;
|
||||||
|
let configPath: string;
|
||||||
|
let tmpDir: string;
|
||||||
|
let setCredsCalls: Array<[string, string]>;
|
||||||
|
let configureCalls: Array<[string | undefined, string | undefined]>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
botDb = createDatabase(":memory:");
|
||||||
|
const users = createUserStore(botDb.db);
|
||||||
|
const sessions = createSessionStore(botDb.db);
|
||||||
|
const alice = await users.createUser("alice", "pw-alice", "admin");
|
||||||
|
cookie = `${SESSION_COOKIE_NAME}=${sessions.createSession(alice.id).token}`;
|
||||||
|
|
||||||
|
tmpDir = mkdtempSync(join(tmpdir(), "botsettings-provider-"));
|
||||||
|
configPath = join(tmpDir, "config.json");
|
||||||
|
config = getDefaultConfig();
|
||||||
|
|
||||||
|
const fakeManager = { getAllBots: () => [] } as unknown as BotManager;
|
||||||
|
const avatarStore = createAvatarStore(tmpDir);
|
||||||
|
|
||||||
|
// Fake search provider recording every setCreds(clientId, clientSecret) call.
|
||||||
|
setCredsCalls = [];
|
||||||
|
const fakeProvider = {
|
||||||
|
setCreds(clientId: string, clientSecret: string) {
|
||||||
|
setCredsCalls.push([clientId, clientSecret]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Fake OAuth so we can assert the playback path is still wired alongside.
|
||||||
|
configureCalls = [];
|
||||||
|
const fakeOAuth = {
|
||||||
|
configure(clientId?: string, redirectUri?: string) {
|
||||||
|
configureCalls.push([clientId, redirectUri]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use("/api", createRequireAuth(sessions, createPermissionStore(botDb.db), () => getDefaultConfig().guestMode));
|
||||||
|
app.use(
|
||||||
|
"/api/bot",
|
||||||
|
createBotRouter(fakeManager, config, configPath, pino({ level: "silent" }), botDb, avatarStore, undefined, fakeOAuth, fakeProvider),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
botDb.close();
|
||||||
|
rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls setCreds once with (clientId, clientSecret) when a spotify block is saved (and OAuth is still configured)", async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/api/bot/settings")
|
||||||
|
.set("Cookie", cookie)
|
||||||
|
.send({ spotify: { clientId: "cid", clientSecret: "sec", enabled: true } });
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(setCredsCalls).toEqual([["cid", "sec"]]);
|
||||||
|
// The OAuth playback path is still wired on the same save.
|
||||||
|
expect(configureCalls.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT call setCreds when the request has no spotify block", async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/api/bot/settings")
|
||||||
|
.set("Cookie", cookie)
|
||||||
|
.send({ autoPauseOnEmpty: false });
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(setCredsCalls).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls setCreds with the PRESERVED stored secret when the spotify block omits/blanks clientSecret", async () => {
|
||||||
|
config.spotify.clientId = "cid0";
|
||||||
|
config.spotify.clientSecret = "stored-secret";
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/api/bot/settings")
|
||||||
|
.set("Cookie", cookie)
|
||||||
|
.send({ spotify: { clientId: "cid0", clientSecret: "", enabled: true } });
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
// Post-merge values: masked/blank secret must keep the stored one, never "".
|
||||||
|
expect(setCredsCalls).toEqual([["cid0", "stored-secret"]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("bot router /settings guest-mode gating + persistence", () => {
|
describe("bot router /settings guest-mode gating + persistence", () => {
|
||||||
let tmpDir: string;
|
let tmpDir: string;
|
||||||
let configPath: string;
|
let configPath: string;
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ export function createBotRouter(
|
|||||||
// I2: the single process-wide OAuth, so a UI-entered Client ID reaches the
|
// I2: the single process-wide OAuth, so a UI-entered Client ID reaches the
|
||||||
// live instance on save (no restart). Structural type = SpotifyOAuth.configure.
|
// live instance on save (no restart). Structural type = SpotifyOAuth.configure.
|
||||||
spotifyOAuth?: { configure(clientId?: string, redirectUri?: string): void },
|
spotifyOAuth?: { configure(clientId?: string, redirectUri?: string): void },
|
||||||
|
// R2-4: the process-wide Web API SEARCH provider. Boot only wires creds when
|
||||||
|
// spotify.enabled is already true, so a fresh install that enables Spotify via
|
||||||
|
// Settings must push creds here too, or search stays empty until a restart.
|
||||||
|
// Structural type = SpotifyProvider.setCreds.
|
||||||
|
spotifyProvider?: { setCreds(clientId: string, clientSecret: string): void },
|
||||||
): Router {
|
): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -135,6 +140,10 @@ export function createBotRouter(
|
|||||||
? `http://127.0.0.1:${config.webPort}/api/spotify/callback`
|
? `http://127.0.0.1:${config.webPort}/api/spotify/callback`
|
||||||
: undefined;
|
: undefined;
|
||||||
spotifyOAuth?.configure(config.spotify.clientId, redirectUri);
|
spotifyOAuth?.configure(config.spotify.clientId, redirectUri);
|
||||||
|
// R2-4: also refresh the live Web API search provider so search/getAuthStatus
|
||||||
|
// work without a restart. Uses the post-merge values so a masked/omitted
|
||||||
|
// secret keeps the stored one. The secret is never logged.
|
||||||
|
spotifyProvider?.setCreds(config.spotify.clientId, config.spotify.clientSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guest-mode changed: tear down / re-scope in-flight guest WS sockets so a
|
// Guest-mode changed: tear down / re-scope in-flight guest WS sockets so a
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { createAuditRouter } from "./api/audit.js";
|
|||||||
import { createFavoritesRouter } from "./api/favorites.js";
|
import { createFavoritesRouter } from "./api/favorites.js";
|
||||||
import { createSpotifyRouter } from "./api/spotify.js";
|
import { createSpotifyRouter } from "./api/spotify.js";
|
||||||
import type { SpotifyOAuth } from "../music/spotify/spotify-oauth.js";
|
import type { SpotifyOAuth } from "../music/spotify/spotify-oauth.js";
|
||||||
|
import type { SpotifyProvider } from "../music/spotify/provider.js";
|
||||||
import { resolveSpotifyBackendKind } from "../music/spotify/backend-select.js";
|
import { resolveSpotifyBackendKind } from "../music/spotify/backend-select.js";
|
||||||
import {
|
import {
|
||||||
isGoLibrespotPresent,
|
isGoLibrespotPresent,
|
||||||
@@ -135,6 +136,11 @@ export function createWebServer(options: WebServerOptions): WebServer {
|
|||||||
// I2: so saving a Client ID in Settings re-configures the live OAuth
|
// I2: so saving a Client ID in Settings re-configures the live OAuth
|
||||||
// (no restart needed for the UI-entered-creds -> Connect flow).
|
// (no restart needed for the UI-entered-creds -> Connect flow).
|
||||||
options.spotifyOAuth,
|
options.spotifyOAuth,
|
||||||
|
// R2-4: so saving Spotify creds in Settings also refreshes the live Web API
|
||||||
|
// search provider (search + getAuthStatus) without a process restart. The
|
||||||
|
// runtime object is a SpotifyProvider (see index.ts); WebServerOptions types
|
||||||
|
// it as the wider MusicProvider, so narrow it here for the setCreds contract.
|
||||||
|
options.spotifyProvider as SpotifyProvider,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
app.use(
|
app.use(
|
||||||
|
|||||||
Reference in new issue
Block a user