import { describe, it, expect, vi } from "vitest"; import { HttpAlertSender, NullAlertSender } from "../src/alerts/sender.js"; import { buildDailyDigest, selectExpiringSoon } from "../src/alerts/digest.js"; import { createAsset, PROVISIONAL_TTL_MS } from "../src/gatekeeper/assets.js"; import { createLogger } from "../src/audit/log.js"; import type { AlertEvent } from "../src/alerts/types.js"; const log = createLogger("silent"); const blocked: AlertEvent = { kind: "blocked_attempt", assetId: "x", tool: "t", status: "bloqué" }; describe("HttpAlertSender", () => { it("poste le payload au webhook", async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200 }); vi.stubGlobal("fetch", fetchMock); await new HttpAlertSender("http://n8n/webhook", log).send(blocked); expect(fetchMock).toHaveBeenCalledOnce(); const init = fetchMock.mock.calls[0]?.[1] as RequestInit; const body = JSON.parse(init.body as string); expect(body).toMatchObject({ source: "chlova", kind: "blocked_attempt", assetId: "x" }); vi.unstubAllGlobals(); }); it("fail-soft : une panne réseau ne lève pas", async () => { vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("boom"))); await expect(new HttpAlertSender("http://n8n/webhook", log).send(blocked)).resolves.toBeUndefined(); vi.unstubAllGlobals(); }); it("NullAlertSender ne lève pas", async () => { await expect(new NullAlertSender(log).send(blocked)).resolves.toBeUndefined(); }); }); describe("digest", () => { it("buildDailyDigest compte bloqués vs provisoires", () => { const items = [ createAsset({ id: "b", type: "tool", version: "1.0.0", riskTier: "privileged" }), createAsset({ id: "r", type: "tool", version: "1.0.0", riskTier: "reversible" }), ]; const d = buildDailyDigest(items); expect(d.blocked).toBe(1); expect(d.provisional).toBe(1); expect(d.items).toHaveLength(2); }); it("selectExpiringSoon ne retient que les provisoires dans la fenêtre J-1", () => { const now = 1_000_000_000; const soon = createAsset({ id: "soon", type: "tool", version: "1.0.0", riskTier: "reversible", now: now - PROVISIONAL_TTL_MS + 3_600_000 }); const far = createAsset({ id: "far", type: "tool", version: "1.0.0", riskTier: "reversible", now }); const result = selectExpiringSoon([soon, far], now); expect(result.map((a) => a.id)).toEqual(["soon"]); }); });