0a2eb203ee
AlertEvent sans secret ; HttpAlertSender (POST webhook n8n, best-effort) + NullAlertSender ; buildDailyDigest / selectExpiringSoon purs. 6 tests. Rattrape l'entrée CHANGELOG 0.15.1 omise. Palier de risque : reversible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
54 lines
2.4 KiB
TypeScript
54 lines
2.4 KiB
TypeScript
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"]);
|
|
});
|
|
});
|