import { jest } from "@jest/globals"; import express from "express"; import request from "supertest"; import { getConfig } from "../common/config.js"; import { apiKeyHandlers } from "./apiKeys.js"; // getConfig() is a mutable singleton — point its enabled modules at a known set. // `dashboard` is enabled but internal, so it must NOT appear as a mintable module. beforeAll(() => { getConfig().appModules = { core: {}, archive: {}, dashboard: { internal: true } }; }); function makeApp({ user = { id: 1, username: "dev" }, store } = {}) { const app = express(); app.use((req, _res, next) => { if (user) req.user = user; next(); }); const h = apiKeyHandlers(store); app.get("/api/dashboard/auth/whoami", h.whoami); app.get("/api/dashboard/auth/keys", h.list); app.post("/api/dashboard/auth/keys", express.json(), h.create); app.delete("/api/dashboard/auth/keys/:id", h.revoke); return app; } const okStore = () => ({ listKeys: jest.fn(async () => [{ id: 1, appType: "core", prefix: "ab12cd34", label: "ci" }]), generateKey: jest.fn(async (_uid, type) => ({ id: 9, key: "rawsecret", prefix: "rawsecre", appType: type })), revokeKey: jest.fn(async (_uid, id) => id === 9), }); describe("API key management routes", () => { it("401s when not logged in", async () => { const res = await request(makeApp({ user: null, store: okStore() })).get("/api/dashboard/auth/keys"); expect(res.status).toBe(401); expect(res.body.error.code).toBe("unauthorized"); }); it("whoami returns username + mintable modules (excludes internal dashboard)", async () => { const res = await request(makeApp({ store: okStore() })).get("/api/dashboard/auth/whoami"); expect(res.status).toBe(200); expect(res.body.username).toBe("dev"); expect(res.body.modules).toEqual(expect.arrayContaining(["core", "archive"])); expect(res.body.modules).not.toContain("dashboard"); }); it("rejects generating a key for an internal module (dashboard)", async () => { const store = okStore(); const res = await request(makeApp({ store })).post("/api/dashboard/auth/keys").send({ type: "dashboard" }); expect(res.status).toBe(400); expect(store.generateKey).not.toHaveBeenCalled(); }); it("lists the user's keys (metadata only)", async () => { const store = okStore(); const res = await request(makeApp({ store })).get("/api/dashboard/auth/keys"); expect(res.status).toBe(200); expect(res.body[0]).toMatchObject({ prefix: "ab12cd34" }); expect(store.listKeys).toHaveBeenCalledWith(1); }); it("generates a key for an enabled module and returns the raw key once", async () => { const store = okStore(); const res = await request(makeApp({ store })) .post("/api/dashboard/auth/keys") .send({ type: "core", label: "ci" }); expect(res.status).toBe(200); expect(res.body.key).toBe("rawsecret"); expect(store.generateKey).toHaveBeenCalledWith(1, "core", { label: "ci", expiresTs: null }); }); it("rejects generating a key for a module that is not enabled", async () => { const store = okStore(); const res = await request(makeApp({ store })).post("/api/dashboard/auth/keys").send({ type: "nope" }); expect(res.status).toBe(400); expect(store.generateKey).not.toHaveBeenCalled(); }); it("rejects a malformed expiresTs with 400 (not a DB-level 500)", async () => { const store = okStore(); const res = await request(makeApp({ store })) .post("/api/dashboard/auth/keys") .send({ type: "core", expiresTs: "not-a-date" }); expect(res.status).toBe(400); expect(store.generateKey).not.toHaveBeenCalled(); }); it("revokes a key (200 when deleted, 404 when not)", async () => { const store = okStore(); const app = makeApp({ store }); expect((await request(app).delete("/api/dashboard/auth/keys/9")).status).toBe(200); expect((await request(app).delete("/api/dashboard/auth/keys/5")).status).toBe(404); }); });