mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
Add the /api/spotify {login,callback,status} router behind the SpotifyOAuthLike
seam (DI-tested with supertest, no network). Build ONE process-wide SpotifyOAuth
in index.ts (clientId/redirectUri from config; store via the already-exported
createFileOAuthTokenStore) and thread that same instance into BOTH createWebServer
AND BotManager -> BotInstance -> SpotifyController, so a web login authorizes
playback (C3.1). Reuses the existing file token store (no token-store.ts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
154 lines
4.7 KiB
TypeScript
154 lines
4.7 KiB
TypeScript
import { describe, it, expect, afterEach } from "vitest";
|
|
import { join } from "node:path";
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { BotManager } from "./manager.js";
|
|
import { createDatabase, type BotDatabase } from "../data/database.js";
|
|
import { createPermissionStore } from "../data/permissions.js";
|
|
import { getDefaultConfig, loadConfig, saveConfig, type BotConfig } from "../data/config.js";
|
|
import type { Logger } from "../logger.js";
|
|
import type { MusicProvider } from "../music/provider.js";
|
|
import type { AvatarStore } from "../data/avatars.js";
|
|
import type { SpotifyOAuth } from "../music/spotify/spotify-oauth.js";
|
|
|
|
// removeBot only calls logger.info; provide the full shape it could touch.
|
|
const stubLogger = {
|
|
info() {},
|
|
warn() {},
|
|
error() {},
|
|
debug() {},
|
|
child() {
|
|
return stubLogger;
|
|
},
|
|
} as unknown as Logger;
|
|
|
|
describe("BotManager.removeBot — guest scope pruning", () => {
|
|
const dirs: string[] = [];
|
|
let db: BotDatabase;
|
|
|
|
function makeTmpConfigPath(): string {
|
|
const dir = mkdtempSync(join(tmpdir(), "tsmusicbot-manager-test-"));
|
|
dirs.push(dir);
|
|
return join(dir, "config.json");
|
|
}
|
|
|
|
function makeManager(config: BotConfig, configPath: string): BotManager {
|
|
db = createDatabase(":memory:");
|
|
const permissions = createPermissionStore(db.db);
|
|
saveConfig(configPath, config);
|
|
return new BotManager(
|
|
{} as unknown as MusicProvider,
|
|
{} as unknown as MusicProvider,
|
|
{} as unknown as MusicProvider,
|
|
db,
|
|
config,
|
|
stubLogger,
|
|
{} as unknown as AvatarStore,
|
|
permissions,
|
|
configPath
|
|
);
|
|
}
|
|
|
|
afterEach(() => {
|
|
try {
|
|
db?.close();
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
for (const d of dirs) {
|
|
rmSync(d, { recursive: true, force: true });
|
|
}
|
|
dirs.length = 0;
|
|
});
|
|
|
|
it("prunes a deleted bot from guestMode.bots (array) and persists", async () => {
|
|
const configPath = makeTmpConfigPath();
|
|
const config = getDefaultConfig();
|
|
config.guestMode.bots = ["botA", "botB"];
|
|
const manager = makeManager(config, configPath);
|
|
|
|
await manager.removeBot("botA");
|
|
|
|
expect(config.guestMode.bots).toEqual(["botB"]);
|
|
// Persisted file must also reflect the prune.
|
|
expect(loadConfig(configPath).guestMode.bots).toEqual(["botB"]);
|
|
});
|
|
|
|
it('leaves guestMode.bots === "all" unchanged (no crash, no change)', async () => {
|
|
const configPath = makeTmpConfigPath();
|
|
const config = getDefaultConfig();
|
|
config.guestMode.bots = "all";
|
|
const manager = makeManager(config, configPath);
|
|
|
|
await manager.removeBot("botA");
|
|
|
|
expect(config.guestMode.bots).toBe("all");
|
|
expect(loadConfig(configPath).guestMode.bots).toBe("all");
|
|
});
|
|
});
|
|
|
|
// --- Spotify OAuth threading (Task 6, C3.1) --------------------------------
|
|
// The single process-wide SpotifyOAuth built in index.ts must reach every bot's
|
|
// SpotifyController: index -> BotManager (trailing positional arg) -> BotInstance
|
|
// -> controller. createBot() builds a REAL (side-effect-free) SpotifyController,
|
|
// so we assert the shared instance surfaces via the controller's getOAuth().
|
|
describe("BotManager — spotifyOAuth threading to bot controllers (C3.1)", () => {
|
|
const dirs: string[] = [];
|
|
let db: BotDatabase | undefined;
|
|
|
|
afterEach(() => {
|
|
try {
|
|
db?.close();
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
db = undefined;
|
|
for (const d of dirs) {
|
|
rmSync(d, { recursive: true, force: true });
|
|
}
|
|
dirs.length = 0;
|
|
});
|
|
|
|
it("forwards its shared SpotifyOAuth into a created bot's controller", async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "tsmusicbot-oauth-thread-"));
|
|
dirs.push(dir);
|
|
const configPath = join(dir, "config.json");
|
|
const config = getDefaultConfig();
|
|
saveConfig(configPath, config);
|
|
db = createDatabase(":memory:");
|
|
const permissions = createPermissionStore(db.db);
|
|
const provider = {} as unknown as MusicProvider;
|
|
const sentinel = {} as unknown as SpotifyOAuth;
|
|
|
|
const manager = new BotManager(
|
|
provider,
|
|
provider,
|
|
provider,
|
|
db,
|
|
config,
|
|
stubLogger,
|
|
{} as unknown as AvatarStore,
|
|
permissions,
|
|
configPath,
|
|
undefined, // localProvider
|
|
undefined, // kugouProvider
|
|
undefined, // spotifyProvider
|
|
join(dir, "spotify"), // spotifyDataDir
|
|
sentinel, // spotifyOAuth (the single shared instance)
|
|
);
|
|
|
|
const bot = await manager.createBot({
|
|
name: "b1",
|
|
serverAddress: "localhost",
|
|
serverPort: 9987,
|
|
nickname: "b1",
|
|
});
|
|
|
|
// Full chain observed: the manager's single shared instance is the exact
|
|
// one the per-bot controller now owns (getOAuth() returns it unchanged).
|
|
expect(bot.getSpotifyController().getOAuth()).toBe(sentinel);
|
|
|
|
bot.disconnect();
|
|
});
|
|
});
|