/** * Unit: the span/log PII + secret scrubber (the phase-15 spec REQUIRES this * assertion: "no stack, no email, no token; redact query text to the * operation shape"). */ import { describe, expect, it } from "vitest"; import { spanScrubber, redactQuery, REDACTED } from "../scrubber.js"; describe("spanScrubber", () => { it("drops stack, email, token, and credential keys outright", () => { const attrs = { stack: "Error: boom\n at x", email: "alice@example.com", token: "abc123", password: "hunter2", authorization: "Bearer x", workspaceId: "ws_1", route: "/api/v1/pages/:id" }; const out = spanScrubber({ ...attrs }); expect(out["stack"]).toBeUndefined(); expect(out["email"]).toBeUndefined(); expect(out["token"]).toBeUndefined(); expect(out["password"]).toBeUndefined(); expect(out["authorization"]).toBeUndefined(); // non-sensitive keys survive expect(out["workspaceId"]).toBe("ws_1"); expect(out["route"]).toBe("/api/v1/pages/:id"); }); it("is case-insensitive on sensitive keys", () => { const out = spanScrubber({ TOKEN: "x", Email: "a@b.co", ApiKey: "k" }); expect(out["TOKEN"]).toBeUndefined(); expect(out["Email"]).toBeUndefined(); expect(out["ApiKey"]).toBeUndefined(); }); it("masks email + bearer + apikey + jwt + URL-cred VALUES even under innocuous keys", () => { const out = spanScrubber({ detail: "login failed for alice@example.com", header: "Bearer sk-abc123def456", cfg: "api_key=abcdef1234567890", id: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4In0.c2ln", url: "https://user:pass@minio.local:9000/bucket" }); const detail = String(out["detail"]); const header = String(out["header"]); const cfg = String(out["cfg"]); const id = String(out["id"]); const url = String(out["url"]); expect(detail).not.toContain("alice@example.com"); expect(detail).toContain(REDACTED); expect(header).not.toContain("sk-abc123def456"); expect(cfg).not.toContain("abcdef1234567890"); expect(id).toContain(REDACTED); expect(url).not.toContain("user:pass@"); expect(url).toContain("[redacted]@"); }); it("redacts query-shaped keys (q/searchQuery/text) to the fixed marker", () => { const out = spanScrubber({ q: "quarterly budget 2026", searchQuery: "secret plan" }); expect(out["q"]).toBe(REDACTED); expect(out["searchQuery"]).toBe(REDACTED); }); it("passes numbers/booleans/ids through and returns the same object", () => { const src = { requestId: "r-1", count: 42, ok: true, email: "a@b.co" }; const out = spanScrubber(src); expect(out).toBe(src); expect(out["requestId"]).toBe("r-1"); expect(out["count"]).toBe(42); expect(out["ok"]).toBe(true); expect(out["email"]).toBeUndefined(); }); }); describe("redactQuery (operation shape)", () => { it("keeps the SQL verbs + table names, drops literals", () => { const out = redactQuery("SELECT id, title FROM blocks WHERE pageId = 'pg_1' AND deletedAt IS NULL LIMIT 50"); expect(out).toContain("SELECT"); expect(out).toContain("blocks"); expect(out).toContain("WHERE"); expect(out).not.toContain("pg_1"); expect(out).not.toContain("50"); }); it("redacts INSERT values to placeholders", () => { const out = redactQuery("INSERT INTO users (email, name) VALUES ('bob@x.co', 'Bob')"); expect(out).toContain("INSERT INTO users"); expect(out).not.toContain("bob@x.co"); expect(out).not.toContain("Bob"); }); it("redacts UPDATE SET values", () => { const out = redactQuery("UPDATE pages SET version = 7, updatedAt = '2026-01-01T00:00:00Z' WHERE id = 'pg_9'"); expect(out).toContain("UPDATE pages SET"); expect(out).not.toContain("7"); expect(out).not.toContain("pg_9"); }); it("collapses whitespace into one shape", () => { const a = redactQuery("SELECT * FROM\nblocks WHERE id = 'x'"); const b = redactQuery("SELECT * FROM blocks WHERE id = 'y'"); expect(a).toBe(b); }); });