import { createPluginContext } from "@suaveplan/api-plugin"; import { type AuditEvent, AuditSeverity, MemoryAuditAdapter, } from "@suaveplan/audit-logging"; import { EventBus } from "@suaveplan/events"; import { describe, expect, it } from "@suaveplan/testing/runner"; import { Hono } from "hono"; import { createAuditPlugin } from "../../src/plugin/plugin.js"; import type { AuditPluginConfig } from "../../src/types/types.js"; const noopLogger = { debug: () => undefined, info: () => undefined, warn: () => undefined, error: () => undefined, }; describe("full-boot integration", () => { it("real api + plugin + sample route records redacted audit with principal and tenant", { timeout: 10000, }, async () => { const sink = new MemoryAuditAdapter(); const plugin = createAuditPlugin({ sink, secretKey: "x".repeat(32), redactQueryParams: ["token"], redactHeaders: ["authorization"], redactBodyFields: ["password"], recordRequestBody: true, }); const app = new Hono(); const services = new Map(); services.set("app", app); // Drive lifecycle hooks the way @suaveplan/api would. const ctx = createPluginContext({ logger: noopLogger, eventBus: new EventBus(), signal: new AbortController().signal, config: plugin.defaults as AuditPluginConfig, services, }); await plugin.onInit?.(ctx); // Pretend auth + tenant plugins have already populated the context. app.use("*", async (c, next) => { c.set("user", { id: "u1" }); c.set("tenant", { id: "t1" }); await next(); }); // Sample route that ALSO fires an explicit semantic event via // c.get("audit") to prove the handler-level helper is wired. app.post("/things/:id", async (c) => { const audit = c.get("audit"); await audit.log({ type: "thing.updated", severity: AuditSeverity.WARNING, actor: { userId: "u1" }, target: { type: "thing", id: c.req.param("id") }, }); return c.json({ updated: true }); }); const res = await app.request("/things/42?token=secret&safe=v", { method: "POST", headers: { Authorization: "Bearer top-secret", "Content-Type": "application/json", }, body: JSON.stringify({ password: "p", note: "hi" }), }); expect(res.status).toBe(200); const events = sink.snapshot(); expect(events.length).toBeGreaterThanOrEqual(2); const requestEvt = events.find((e) => String(e.type).startsWith("http.") ) as AuditEvent | undefined; expect(requestEvt).toBeDefined(); const det = requestEvt?.details as { method: string; path: string; status: number; duration_ms: number; query: Record; headers: Record; body?: Record; }; expect(det.method).toBe("POST"); expect(det.path).toBe("/things/42"); expect(det.status).toBe(200); expect(det.duration_ms).toBeGreaterThanOrEqual(0); expect(det.query["token"]).toBe("[REDACTED]"); expect(det.query["safe"]).toBe("v"); expect(det.headers["authorization"]).toBe("[REDACTED]"); expect(det.body?.["password"]).toBe("[REDACTED]"); expect(det.body?.["note"]).toBe("hi"); expect(requestEvt?.actor.userId).toBe("u1"); expect(requestEvt?.tenantId).toBe("t1"); const semanticEvt = events.find((e) => e.type === "thing.updated") as | AuditEvent | undefined; expect(semanticEvt).toBeDefined(); expect(semanticEvt?.target?.id).toBe("42"); }); });