26debf2fe0
registerApi : login rate-limité → JWT, puis Bearer obligatoire sur /api/chat, /api/review (+approve/refuse), /api/state. CORS restreint + rate-limit. Réutilise ChatService + ReviewService. API activée seulement si auth configurée (apiAuth). 65 tests (5 via inject), 0 vuln. Palier de risque : privilégié (surface exposée) — montée seulement si auth présente ; non exposée tant que .env non renseigné. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
103 lines
3.5 KiB
TypeScript
103 lines
3.5 KiB
TypeScript
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<string> {
|
|
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);
|
|
});
|
|
});
|