1
6import { describe, expect, it } from "vitest";
7import { spanScrubber, redactQuery, REDACTED } from "../scrubber.js";
8
9describe("spanScrubber", () => {
10 it("drops stack, email, token, and credential keys outright", () => {
11 const attrs = {
12 stack: "Error: boom\n at x",
13 email: "alice@example.com",
14 token: "abc123",
15 password: "hunter2",
16 authorization: "Bearer x",
17 workspaceId: "ws_1",
18 route: "/api/v1/pages/:id"
19 };
20 const out = spanScrubber({ ...attrs });
21 expect(out["stack"]).toBeUndefined();
22 expect(out["email"]).toBeUndefined();
23 expect(out["token"]).toBeUndefined();
24 expect(out["password"]).toBeUndefined();
25 expect(out["authorization"]).toBeUndefined();
26
27 expect(out["workspaceId"]).toBe("ws_1");
28 expect(out["route"]).toBe("/api/v1/pages/:id");
29 });
30
31 it("is case-insensitive on sensitive keys", () => {
32 const out = spanScrubber({ TOKEN: "x", Email: "a@b.co", ApiKey: "k" });
33 expect(out["TOKEN"]).toBeUndefined();
34 expect(out["Email"]).toBeUndefined();
35 expect(out["ApiKey"]).toBeUndefined();
36 });
37
38 it("masks email + bearer + apikey + jwt + URL-cred VALUES even under innocuous keys", () => {
39 const out = spanScrubber({
40 detail: "login failed for alice@example.com",
41 header: "Bearer sk-abc123def456",
42 cfg: "api_key=abcdef1234567890",
43 id: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4In0.c2ln",
44 url: "https://user:pass@minio.local:9000/bucket"
45 });
46 const detail = String(out["detail"]);
47 const header = String(out["header"]);
48 const cfg = String(out["cfg"]);
49 const id = String(out["id"]);
50 const url = String(out["url"]);
51 expect(detail).not.toContain("alice@example.com");
52 expect(detail).toContain(REDACTED);
53 expect(header).not.toContain("sk-abc123def456");
54 expect(cfg).not.toContain("abcdef1234567890");
55 expect(id).toContain(REDACTED);
56 expect(url).not.toContain("user:pass@");
57 expect(url).toContain("[redacted]@");
58 });
59
60 it("redacts query-shaped keys (q/searchQuery/text) to the fixed marker", () => {
61 const out = spanScrubber({ q: "quarterly budget 2026", searchQuery: "secret plan" });
62 expect(out["q"]).toBe(REDACTED);
63 expect(out["searchQuery"]).toBe(REDACTED);
64 });
65
66 it("passes numbers/booleans/ids through and returns the same object", () => {
67 const src = { requestId: "r-1", count: 42, ok: true, email: "a@b.co" };
68 const out = spanScrubber(src);
69 expect(out).toBe(src);
70 expect(out["requestId"]).toBe("r-1");
71 expect(out["count"]).toBe(42);
72 expect(out["ok"]).toBe(true);
73 expect(out["email"]).toBeUndefined();
74 });
75});
76
77describe("redactQuery (operation shape)", () => {
78 it("keeps the SQL verbs + table names, drops literals", () => {
79 const out = redactQuery("SELECT id, title FROM blocks WHERE pageId = 'pg_1' AND deletedAt IS NULL LIMIT 50");
80 expect(out).toContain("SELECT");
81 expect(out).toContain("blocks");
82 expect(out).toContain("WHERE");
83 expect(out).not.toContain("pg_1");
84 expect(out).not.toContain("50");
85 });
86
87 it("redacts INSERT values to placeholders", () => {
88 const out = redactQuery("INSERT INTO users (email, name) VALUES ('bob@x.co', 'Bob')");
89 expect(out).toContain("INSERT INTO users");
90 expect(out).not.toContain("bob@x.co");
91 expect(out).not.toContain("Bob");
92 });
93
94 it("redacts UPDATE SET values", () => {
95 const out = redactQuery("UPDATE pages SET version = 7, updatedAt = '2026-01-01T00:00:00Z' WHERE id = 'pg_9'");
96 expect(out).toContain("UPDATE pages SET");
97 expect(out).not.toContain("7");
98 expect(out).not.toContain("pg_9");
99 });
100
101 it("collapses whitespace into one shape", () => {
102 const a = redactQuery("SELECT * FROM\nblocks WHERE id = 'x'");
103 const b = redactQuery("SELECT * FROM blocks WHERE id = 'y'");
104 expect(a).toBe(b);
105 });
106});
107