96 lines
3.9 KiB
JavaScript
96 lines
3.9 KiB
JavaScript
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);
|
|
});
|
|
});
|