// Smoke test: boots the API layer with a stubbed Stoat context and exercises // the auth flow + a couple of routes. No real Stoat instance involved. process.env.STOAT_API_URL = "http://127.0.0.1:9/api"; process.env.STOAT_BOT_TOKEN = "test-token"; process.env.PUBLIC_URL = "http://127.0.0.1:3199"; process.env.JWT_SECRET = "0123456789abcdef0123456789abcdef"; process.env.PORT = "3199"; process.env.LOG_LEVEL = "warn"; process.env.NODE_ENV = "production"; const { startApiServer } = await import("../dist/api/server.js"); const { MusicManager } = await import("../dist/core/manager.js"); const { signPanelLink } = await import("../dist/auth/tokens.js"); const context = { getServerName: () => "Test Server", getVoiceChannel: (id) => ({ id, name: "General" }), listVoiceChannels: () => [{ id: "vc1", name: "General" }], findUserVoiceChannel: () => ({ id: "vc1", name: "General" }), hasVoicePresence: () => true, listServersForUser: async () => [{ id: "srv1", name: "Test Server", iconUrl: null }], isMember: async (s, u) => s === "srv1" && u === "user1", canControl: async () => true, sendMessage: async () => {}, }; const manager = new MusicManager(); manager.attachStoat(context); const app = await startApiServer({ manager, context }); const base = "http://127.0.0.1:3199"; const results = []; const check = (name, ok, detail = "") => results.push({ name, ok, detail }); // 1. Unauthenticated access is rejected. let res = await fetch(`${base}/api/me`); check("GET /api/me without cookie → 401", res.status === 401, `got ${res.status}`); // 2. One-time chat link exchanges for a session cookie. const link = await signPanelLink("user1", "tester", "srv1"); res = await fetch(`${base}/api/auth/link`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ token: link }), }); const linkBody = await res.json(); const cookie = (res.headers.getSetCookie?.() ?? [])[0]?.split(";")[0] ?? ""; check("POST /api/auth/link → session", res.ok && linkBody.serverId === "srv1" && cookie.length > 0, JSON.stringify(linkBody)); // 3. Authenticated profile lists the servers we share with the bot. res = await fetch(`${base}/api/me`, { headers: { cookie } }); const me = await res.json(); check("GET /api/me with cookie", res.ok && me.servers?.[0]?.id === "srv1", JSON.stringify(me)); // 4. Player state for a server the user belongs to. res = await fetch(`${base}/api/servers/srv1/state`, { headers: { cookie } }); const state = await res.json(); check( "GET state", res.ok && state.state.status === "idle" && state.voiceChannels.length === 1, JSON.stringify(state).slice(0, 160), ); // 5. A server the user is not a member of stays hidden. res = await fetch(`${base}/api/servers/other/state`, { headers: { cookie } }); check("GET state for foreign server → 403", res.status === 403, `got ${res.status}`); // 6. An expired/garbage link is refused. res = await fetch(`${base}/api/auth/link`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ token: "not-a-jwt" }), }); check("POST /api/auth/link with junk → 400", res.status === 400, `got ${res.status}`); // 7. Actions on a server with no player report a clear error, not a crash. res = await fetch(`${base}/api/servers/srv1/actions/pause`, { method: "POST", headers: { "content-type": "application/json", cookie }, body: "{}", }); const pause = await res.json(); check("POST pause without player → 400", res.status === 400 && typeof pause.error === "string", JSON.stringify(pause)); // 8. Unknown action. res = await fetch(`${base}/api/servers/srv1/actions/explode`, { method: "POST", headers: { "content-type": "application/json", cookie }, body: "{}", }); check("POST unknown action → 404", res.status === 404, `got ${res.status}`); // 9. Static panel bundle is served. res = await fetch(`${base}/`); const html = await res.text(); check("GET / serves panel", res.ok && html.includes("