import { describe, it, expect, beforeEach, afterEach } from "vitest"; import Fastify, { type FastifyInstance } from "fastify"; import { generateSync, generateSecret } from "otplib"; import { registerApi } from "../src/api/routes.js"; import { hashPassword, type AuthConfig } from "../src/api/auth.js"; import { ChatService } from "../src/agent/chat-service.js"; import { ReviewService } from "../src/gatekeeper/review.js"; import { AssetRepository } from "../src/gatekeeper/repository.js"; import { createAsset } from "../src/gatekeeper/assets.js"; import { createLogger } from "../src/audit/log.js"; const log = createLogger("silent"); const auth: AuthConfig = { adminUser: "kantin", adminPasswordHash: hashPassword("pw"), totpSecret: generateSecret(), jwtSecret: "jwt-secret-suffisamment-long-pour-hs256", }; // ChatService stub : pas de vrai LLM. const chatStub = { handle: async (_actor: string, text: string) => `echo:${text}`, } as unknown as ChatService; let app: FastifyInstance; let repo: AssetRepository; beforeEach(async () => { repo = new AssetRepository(":memory:"); const review = new ReviewService(repo, log); app = Fastify(); await registerApi(app, { auth, chat: chatStub, review, state: () => ({ phase: "2-write-review", tools: 3 }) }); await app.ready(); }); afterEach(async () => { await app.close(); repo.close(); }); async function tokenOf(): Promise { const res = await app.inject({ method: "POST", url: "/api/auth/login", payload: { user: "kantin", password: "pw", totp: generateSync({ secret: auth.totpSecret }) }, }); return res.json().token as string; } describe("API auth", () => { it("login renvoie un token avec bons facteurs", async () => { const res = await app.inject({ method: "POST", url: "/api/auth/login", payload: { user: "kantin", password: "pw", totp: generateSync({ secret: auth.totpSecret }) }, }); expect(res.statusCode).toBe(200); expect(res.json().token).toBeTruthy(); }); it("login 401 avec mauvais mot de passe", async () => { const res = await app.inject({ method: "POST", url: "/api/auth/login", payload: { user: "kantin", password: "bad", totp: generateSync({ secret: auth.totpSecret }) }, }); expect(res.statusCode).toBe(401); }); it("/api/chat refuse sans token (401)", async () => { const res = await app.inject({ method: "POST", url: "/api/chat", payload: { message: "salut" } }); expect(res.statusCode).toBe(401); }); it("/api/chat répond avec token", async () => { const token = await tokenOf(); const res = await app.inject({ method: "POST", url: "/api/chat", headers: { authorization: `Bearer ${token}` }, payload: { message: "salut" }, }); expect(res.statusCode).toBe(200); expect(res.json().reply).toBe("echo:salut"); }); }); describe("API review", () => { it("liste, approuve et refuse", async () => { repo.create(createAsset({ id: "tool:x", type: "tool", version: "1.0.0", riskTier: "privileged" })); const token = await tokenOf(); const auth_h = { authorization: `Bearer ${token}` }; const list = await app.inject({ method: "GET", url: "/api/review", headers: auth_h }); expect(list.json().assets).toHaveLength(1); const ok = await app.inject({ method: "POST", url: "/api/review/tool:x/approve", headers: auth_h }); expect(ok.json().asset.status).toBe("approuvé"); const missing = await app.inject({ method: "POST", url: "/api/review/nope/refuse", headers: auth_h }); expect(missing.statusCode).toBe(404); }); });