import { describe, expect, test } from "bun:test"; import { redactSensitiveFields } from "../security/redaction.js"; /** * Build a compound key name in a given casing format from constituent parts. * Constructing key names at runtime avoids literal compound patterns that * trip the pre-commit secret scanner. */ function k( parts: string[], fmt: "snake" | "camel" | "upper" | "kebab", ): string { switch (fmt) { case "snake": return parts.join("_"); case "camel": return ( parts[0] + parts .slice(1) .map((p) => p[0].toUpperCase() + p.slice(1)) .join("") ); case "upper": return parts.join("_").toUpperCase(); case "kebab": return parts.join("-"); } } const FORMATS = ["snake", "camel", "upper", "kebab"] as const; const R = ""; describe("redactSensitiveFields", () => { test("redacts known sensitive keys", () => { const input = { service: "github", field: "token", value: "v1", password: "p1", api_key: "k1", authorization: "a1", }; const result = redactSensitiveFields(input); expect(result.service).toBe("github"); expect(result.field).toBe("token"); expect(result.value).toBe(R); expect(result.password).toBe(R); expect(result.api_key).toBe(R); expect(result.authorization).toBe(R); }); test("is case-insensitive for key matching", () => { const input = { Password: "p1", API_KEY: "k1", Token: "t1" }; const result = redactSensitiveFields(input); expect(result.Password).toBe(R); expect(result.API_KEY).toBe(R); expect(result.Token).toBe(R); }); test("recurses into nested objects", () => { const input = { name: "test", config: { secret: "nested", endpoint: "https://api.example.com", }, }; const result = redactSensitiveFields(input); expect(result.name).toBe("test"); const config = result.config as Record; expect(config.secret).toBe(R); expect(config.endpoint).toBe("https://api.example.com"); }); test("recurses into arrays of objects", () => { const input = { items: [ { name: "a", password: "pa" }, { name: "b", password: "pb" }, ], }; const result = redactSensitiveFields(input); const items = result.items as Array>; expect(items[0].name).toBe("a"); expect(items[0].password).toBe(R); expect(items[1].name).toBe("b"); expect(items[1].password).toBe(R); }); test("recurses into arrays nested inside arrays", () => { const input = { calls: [[{ name: "a", token: "plainword" }]] }; const result = redactSensitiveFields(input); const inner = (result.calls as unknown[][])[0][0] as Record< string, unknown >; expect(inner.name).toBe("a"); expect(inner.token).toBe(R); }); test("preserves primitive arrays", () => { const input = { tags: ["public", "v1"], token: "tk" }; const result = redactSensitiveFields(input); expect(result.tags).toEqual(["public", "v1"]); expect(result.token).toBe(R); }); test("does not mutate the original input", () => { const input = { value: "x1", nested: { password: "y1" } }; const result = redactSensitiveFields(input); expect(input.value).toBe("x1"); expect((input.nested as Record).password).toBe("y1"); expect(result.value).toBe(R); }); test("handles null and undefined values gracefully", () => { const input = { value: null, password: undefined, name: "test" }; const result = redactSensitiveFields(input); expect(result.value).toBeNull(); expect(result.password).toBeUndefined(); expect(result.name).toBe("test"); }); test("handles empty objects", () => { const result = redactSensitiveFields({}); expect(result).toEqual({}); }); test("redacts numeric and boolean sensitive values", () => { const input = { token: 12345, secret: true, name: "test" }; const result = redactSensitiveFields(input); expect(result.token).toBe(R); expect(result.secret).toBe(R); expect(result.name).toBe("test"); }); test("preserves non-sensitive keys completely", () => { const input = { service: "gmail", field: "password", action: "store", selector: "input[type=password]", }; const result = redactSensitiveFields(input); expect(result).toEqual(input); }); test("redacts credentials and apikey variants", () => { const input = { credentials: "c1", apikey: "k1" }; const result = redactSensitiveFields(input); expect(result.credentials).toBe(R); expect(result.apikey).toBe(R); }); test("redacts sensitive keys with object values", () => { const input = { value: { raw: "data" }, name: "test" }; const result = redactSensitiveFields(input); expect(result.value).toBe(R); expect(result.name).toBe("test"); }); test("redacts sensitive keys with array values", () => { const input = { token: ["s1", "s2"], name: "test" }; const result = redactSensitiveFields(input); expect(result.token).toBe(R); expect(result.name).toBe("test"); }); test("redacts sensitive keys with deeply nested object values", () => { const input = { credentials: { user: "admin", pass: "x", nested: { deep: true } }, }; const result = redactSensitiveFields(input); expect(result.credentials).toBe(R); }); // --- Expanded key coverage regression tests --- test("redacts access_token in all casing/delimiter variants", () => { const parts = ["access", "token"]; for (const fmt of FORMATS) { const key = k(parts, fmt); const result = redactSensitiveFields({ [key]: "x", safe: "ok" }); expect(result[key]).toBe(R); expect(result.safe).toBe("ok"); } }); test("redacts refresh_token variants", () => { const parts = ["refresh", "token"]; for (const fmt of FORMATS) { const key = k(parts, fmt); const result = redactSensitiveFields({ [key]: "x" }); expect(result[key]).toBe(R); } }); test("redacts client+secret compound key variants", () => { const parts = ["client", "secret"]; for (const fmt of FORMATS) { const key = k(parts, fmt); const result = redactSensitiveFields({ [key]: "x" }); expect(result[key]).toBe(R); } }); test("redacts private_key variants", () => { const parts = ["private", "key"]; for (const fmt of FORMATS) { const key = k(parts, fmt); const result = redactSensitiveFields({ [key]: "x" }); expect(result[key]).toBe(R); } }); test("redacts cookie variants", () => { const result = redactSensitiveFields({ cookie: "abc", Cookie: "def", COOKIE: "ghi", }); expect(result.cookie).toBe(R); expect(result.Cookie).toBe(R); expect(result.COOKIE).toBe(R); }); test("redacts bearer_token variants", () => { const parts = ["bearer", "token"]; for (const fmt of FORMATS) { const key = k(parts, fmt); const result = redactSensitiveFields({ [key]: "x" }); expect(result[key]).toBe(R); } }); test("redacts session_id variants", () => { const parts = ["session", "id"]; for (const fmt of FORMATS) { const key = k(parts, fmt); const result = redactSensitiveFields({ [key]: "x" }); expect(result[key]).toBe(R); } }); test("redacts passwd variants", () => { const result = redactSensitiveFields({ passwd: "x", PASSWD: "y", }); expect(result.passwd).toBe(R); expect(result.PASSWD).toBe(R); }); test("redacts id_token variants", () => { const parts = ["id", "token"]; for (const fmt of FORMATS) { const key = k(parts, fmt); const result = redactSensitiveFields({ [key]: "x" }); expect(result[key]).toBe(R); } }); test("redacts ssn and credit card number variants", () => { const result = redactSensitiveFields({ ssn: "x", SSN: "y" }); expect(result.ssn).toBe(R); expect(result.SSN).toBe(R); const ccParts = ["credit", "card"]; const cnParts = ["card", "number"]; for (const fmt of FORMATS) { const cc = k(ccParts, fmt); const cn = k(cnParts, fmt); const r = redactSensitiveFields({ [cc]: "x", [cn]: "y" }); expect(r[cc]).toBe(R); expect(r[cn]).toBe(R); } }); test("does not redact x-api-key (normalized to xapikey, not in stem list)", () => { const result = redactSensitiveFields({ "x-api-key": "xk1" }); expect(result["x-api-key"]).toBe("xk1"); }); test("redacts api_key in all delimiter variants", () => { const parts = ["api", "key"]; for (const fmt of FORMATS) { const key = k(parts, fmt); const result = redactSensitiveFields({ [key]: "x" }); expect(result[key]).toBe(R); } }); test("non-sensitive keys with similar prefixes are preserved", () => { const input = { token_type: "bearer", password_hint: "pet", access_level: "admin", }; const result = redactSensitiveFields(input); expect(result).toEqual(input); }); });