Files
teamspeak-music-bot/src/data/database.test.ts
T
2026-06-25 11:30:12 +08:00

180 lines
5.9 KiB
TypeScript

import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { createDatabase, type BotDatabase, type BotInstance, type PlayHistoryEntry } from "./database.js";
import { createUserStore, GUEST_USER_ID } from "./users.js";
describe("database", () => {
let botDb: BotDatabase;
beforeEach(() => {
botDb = createDatabase(":memory:");
});
afterEach(() => {
botDb.close();
});
it("creates tables on init", () => {
const tables = botDb.db
.prepare(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
)
.all() as Array<{ name: string }>;
const names = tables.map((t) => t.name);
expect(names).toContain("play_history");
expect(names).toContain("bot_instances");
});
it("creates users and sessions tables on init", () => {
const tables = botDb.db
.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
.all() as Array<{ name: string }>;
const names = tables.map((t) => t.name);
expect(names).toContain("users");
expect(names).toContain("sessions");
const userCols = botDb.db.prepare("PRAGMA table_info(users)").all() as Array<{ name: string }>;
const userColNames = userCols.map((c) => c.name).sort();
expect(userColNames).toEqual(["createdAt", "id", "passwordHash", "role", "updatedAt", "username"]);
const sessionCols = botDb.db.prepare("PRAGMA table_info(sessions)").all() as Array<{ name: string }>;
const sessionColNames = sessionCols.map((c) => c.name).sort();
expect(sessionColNames).toEqual(["createdAt", "expiresAt", "id", "lastSeenAt", "userId"]);
});
it("creates user_audit table on init", () => {
const tables = botDb.db
.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
.all() as Array<{ name: string }>;
expect(tables.map((t) => t.name)).toContain("user_audit");
});
it("records and retrieves play history", () => {
botDb.addPlayHistory({
botId: "bot1",
songId: "song1",
songName: "Test Song",
artist: "Test Artist",
album: "Test Album",
platform: "netease",
coverUrl: "https://example.com/cover.jpg",
});
botDb.addPlayHistory({
botId: "bot1",
songId: "song2",
songName: "Another Song",
artist: "Another Artist",
album: "Another Album",
platform: "qq",
coverUrl: "https://example.com/cover2.jpg",
});
const history = botDb.getPlayHistory("bot1", 10);
expect(history).toHaveLength(2);
expect(history[0].songName).toBe("Another Song");
expect(history[1].songName).toBe("Test Song");
});
it("saves and loads bot instances", () => {
const instance: BotInstance = {
id: "bot1",
name: "Music Bot",
serverAddress: "localhost",
serverPort: 9987,
nickname: "MusicBot",
defaultChannel: "Music",
channelId: "",
channelPassword: "",
autoStart: true,
serverProtocol: "",
ts6ApiKey: "",
serverPassword: "",
};
botDb.saveBotInstance(instance);
const instances = botDb.getBotInstances();
expect(instances).toHaveLength(1);
expect(instances[0]).toMatchObject(instance);
expect(instances[0].autoStart).toBe(true);
// Test upsert
botDb.saveBotInstance({ ...instance, nickname: "UpdatedBot", autoStart: false });
const updated = botDb.getBotInstances();
expect(updated).toHaveLength(1);
expect(updated[0].nickname).toBe("UpdatedBot");
expect(updated[0].autoStart).toBe(false);
});
it("deletes bot instance", () => {
botDb.saveBotInstance({
id: "bot1",
name: "Music Bot",
serverAddress: "localhost",
serverPort: 9987,
nickname: "MusicBot",
defaultChannel: "Music",
channelId: "",
channelPassword: "",
autoStart: false,
serverProtocol: "",
ts6ApiKey: "",
serverPassword: "",
});
expect(botDb.deleteBotInstance("bot1")).toBe(true);
expect(botDb.getBotInstances()).toHaveLength(0);
expect(botDb.deleteBotInstance("nonexistent")).toBe(false);
});
it("persists and clears customAvatarPath on a bot instance", () => {
const inst = {
id: "bot-1",
name: "B",
serverAddress: "x",
serverPort: 9987,
nickname: "n",
defaultChannel: "",
channelId: "",
channelPassword: "",
autoStart: false,
serverProtocol: "",
ts6ApiKey: "",
serverPassword: "",
};
botDb.saveBotInstance(inst);
expect(botDb.getCustomAvatarPath("bot-1")).toBeNull();
botDb.setCustomAvatarPath("bot-1", "avatars/bot-1.png");
expect(botDb.getCustomAvatarPath("bot-1")).toBe("avatars/bot-1.png");
botDb.setCustomAvatarPath("bot-1", null);
expect(botDb.getCustomAvatarPath("bot-1")).toBeNull();
});
});
describe("guest principal migration", () => {
it("creates exactly one reserved guest row, idempotently", () => {
const dir = mkdtempSync(join(tmpdir(), "tsmb-db-"));
const p = join(dir, "t.db");
const a = createDatabase(p); a.db.close();
const b = createDatabase(p); // run again — must not duplicate
const row = b.db.prepare("SELECT id, role FROM users WHERE id = ?").get(GUEST_USER_ID) as { id: string; role: string } | undefined;
expect(row?.role).toBe("guest");
const n = (b.db.prepare("SELECT COUNT(*) AS n FROM users WHERE role='guest'").get() as { n: number }).n;
expect(n).toBe(1);
b.db.close();
rmSync(dir, { recursive: true, force: true });
});
it("guest row does not break first-run detection (countUsers excludes it)", () => {
const dir = mkdtempSync(join(tmpdir(), "tsmb-db2-"));
const p = join(dir, "t.db");
const d = createDatabase(p);
const users = createUserStore(d.db);
expect(users.countUsers()).toBe(0); // guest excluded → still needs setup
d.db.close();
rmSync(dir, { recursive: true, force: true });
});
});