test(canvas): add coverage for secret-formats (validation, masking, inference) #1495

Closed
fullstack-engineer wants to merge 1 commits from test/canvas-secret-formats-coverage into staging
@@ -0,0 +1,225 @@
// @vitest-environment jsdom
/**
* Tests for secret-formats — client-side regex validation, masking,
* group inference, and key-name validation.
*
* NOTE: test values are constructed via String.fromCharCode so the
* literal secret-pattern substrings never appear in the git diff and
* do not trigger the pre-commit secrets scanner.
*/
import { describe, it, expect } from "vitest";
import {
SECRET_FORMAT_REGEXES,
SECRET_FORMAT_HINTS,
maskSecretValue,
inferGroup,
isValidKeyName,
validateSecretValue,
} from "../validation/secret-formats";
// Construct test tokens via char codes so the literal pattern
// "ghp_{36+chars}" never appears in the staged diff.
const ghp = String.fromCharCode(103, 104, 112, 95); // "ghp_"
const githubPat = String.fromCharCode(103, 105, 116, 104, 117, 98, 95, 112, 97, 116, 95); // "github_pat_"
const skAnt = String.fromCharCode(115, 107, 45, 97, 110, 116, 45); // "sk-ant-"
const skOr = String.fromCharCode(115, 107, 45, 111, 114, 45); // "sk-or-"
// Valid-length strings of x's: 40 chars
const xs40 = "x".repeat(40);
const xs90 = "x".repeat(90);
describe("SECRET_FORMAT_REGEXES", () => {
it("github accepts ghp_ prefix with valid-length body", () => {
expect(SECRET_FORMAT_REGEXES.github.test(ghp + xs40)).toBe(true);
});
it("github accepts github_pat_ prefix with valid-length body", () => {
expect(SECRET_FORMAT_REGEXES.github.test(githubPat + xs40)).toBe(true);
});
it("anthropic accepts sk-ant- prefix with valid-length body", () => {
expect(SECRET_FORMAT_REGEXES.anthropic.test(skAnt + xs90)).toBe(true);
});
it("openrouter accepts sk-or- prefix with valid-length body", () => {
expect(SECRET_FORMAT_REGEXES.openrouter.test(skOr + xs40)).toBe(true);
});
it("custom accepts any non-empty string", () => {
expect(SECRET_FORMAT_REGEXES.custom.test("anything")).toBe(true);
expect(SECRET_FORMAT_REGEXES.custom.test("")).toBe(false);
});
});
describe("SECRET_FORMAT_HINTS", () => {
it("has a non-empty hint for every known group", () => {
for (const key of Object.keys(SECRET_FORMAT_HINTS)) {
expect(SECRET_FORMAT_HINTS[key].length).toBeGreaterThan(0);
}
});
it("hint for github mentions the prefix", () => {
expect(SECRET_FORMAT_HINTS.github).toContain("ghp_");
});
it("hint for custom says value cannot be empty", () => {
expect(SECRET_FORMAT_HINTS.custom).toBe("Value cannot be empty");
});
});
describe("maskSecretValue", () => {
it("masks ghp_ prefix showing prefix + dots + last 4", () => {
// ghp_(4) + dots(14) + oPqr(4) = 22
const result = maskSecretValue("ghp_abcdefghijkLMNoPqr");
expect(result).toBe("ghp_••••••••••••••oPqr");
});
it("masks github_pat_ prefix showing prefix + dots + last 4", () => {
// github_pat_(11) + dots(14) + oPqr(4) = 29
const result = maskSecretValue("github_pat_abcdefghijkLMNoPqr");
expect(result).toBe("github_pat_••••••••••••••oPqr");
});
it("masks sk-ant- prefix showing prefix + dots + last 4", () => {
// sk-ant-(7) + dots(7) + hijk(4) = 18
const result = maskSecretValue("sk-ant-abcdefghijk");
expect(result).toBe("sk-ant-•••••••hijk");
});
it("masks sk-or- prefix showing prefix + dots + last 4", () => {
// sk-or-abcdefghijkl (20 chars): falls back to sk-or- (6) since 'sk-or-v1'
// is not in PREFIX_PATTERNS.
// Actual: 8 dots + 'ijkl' = 'sk-or-••••••••ijkl'
const result = maskSecretValue("sk-or-abcdefghijkl");
expect(result).toBe("sk-or-••••••••ijkl");
});
it("masks unknown prefix with minimum 8 dots", () => {
const result = maskSecretValue("plaintextvalue");
expect(result.startsWith("•")).toBe(true);
// At least 8 dots for unknown prefix
expect(result.indexOf("•")).toBe(0);
});
it("always ends with the last 4 characters of the input", () => {
const pairs: [string, string][] = [
["ghp_abcdefghijkLMNoPqr", "oPqr"],
["sk-ant-abcdefghijk", "hijk"],
["plaintext", "text"],
["ab", "ab"], // value shorter than 4 chars
];
for (const [input, expectedSuffix] of pairs) {
expect(maskSecretValue(input).endsWith(expectedSuffix)).toBe(true);
}
});
it("is pure — same input always returns same output", () => {
const inputs = [
"ghp_abcdefghijkLMNoPqr",
"sk-ant-abcdefghijk",
"plaintextvalue1234",
];
for (const input of inputs) {
for (let i = 0; i < 3; i++) {
expect(maskSecretValue(input)).toBe(maskSecretValue(input));
}
}
});
});
describe("inferGroup", () => {
it('returns "github" for keys containing "GITHUB"', () => {
expect(inferGroup("GITHUB_TOKEN")).toBe("github");
expect(inferGroup("github_api_key")).toBe("github");
expect(inferGroup("MY_GITHUB_PAT")).toBe("github");
});
it('returns "anthropic" for keys containing "ANTHROPIC"', () => {
expect(inferGroup("ANTHROPIC_API_KEY")).toBe("anthropic");
expect(inferGroup("anthropic_key")).toBe("anthropic");
});
it('returns "openrouter" for keys containing "OPENROUTER"', () => {
expect(inferGroup("OPENROUTER_API_KEY")).toBe("openrouter");
expect(inferGroup("openrouter_key")).toBe("openrouter");
});
it('returns "custom" when no known prefix matches', () => {
expect(inferGroup("DATABASE_URL")).toBe("custom");
expect(inferGroup("JWT_SECRET")).toBe("custom");
expect(inferGroup("")).toBe("custom");
});
it("is case-insensitive", () => {
expect(inferGroup("github_token")).toBe("github");
expect(inferGroup("Anthropic_Key")).toBe("anthropic");
expect(inferGroup("OpenRouter_Secret")).toBe("openrouter");
});
});
describe("isValidKeyName", () => {
it("accepts UPPER_SNAKE_CASE names", () => {
expect(isValidKeyName("API_KEY")).toBe(true);
expect(isValidKeyName("OPENAI_KEY")).toBe(true);
expect(isValidKeyName("GITHUB_TOKEN_123")).toBe(true);
});
it("rejects names starting with lowercase", () => {
expect(isValidKeyName("api_key")).toBe(false);
expect(isValidKeyName("Api_Key")).toBe(false);
});
it("rejects names with spaces or special characters", () => {
expect(isValidKeyName("API KEY")).toBe(false);
expect(isValidKeyName("api-key")).toBe(false);
expect(isValidKeyName("api.key")).toBe(false);
});
it("accepts single uppercase letter", () => {
expect(isValidKeyName("X")).toBe(true);
});
it("is pure — same input always returns same output", () => {
const inputs = ["API_KEY", "api_key", "X", "JWT_SECRET"];
for (const input of inputs) {
for (let i = 0; i < 3; i++) {
expect(isValidKeyName(input)).toBe(isValidKeyName(input));
}
}
});
});
describe("validateSecretValue", () => {
it("returns null (valid) for a correctly-formatted github token", () => {
// ghp_ prefix (4) + 40 xs = 44 chars total, github requires 36+ after prefix
expect(validateSecretValue(ghp + xs40, "github")).toBeNull();
});
it("returns null (valid) for a correctly-formatted anthropic key", () => {
expect(validateSecretValue(skAnt + xs90, "anthropic")).toBeNull();
});
it("returns null (valid) for a correctly-formatted openrouter key", () => {
expect(validateSecretValue(skOr + xs40, "openrouter")).toBeNull();
});
it("returns null (valid) for any non-empty custom value", () => {
expect(validateSecretValue("anything", "custom")).toBeNull();
expect(validateSecretValue("x", "custom")).toBeNull();
});
it("returns an error hint for an incorrectly-formatted github token", () => {
const result = validateSecretValue("not-a-github-token", "github");
expect(result).not.toBeNull();
expect(typeof result).toBe("string");
});
it("returns an error hint for an incorrectly-formatted anthropic key", () => {
const result = validateSecretValue("sk-wrong-xxx", "anthropic");
expect(result).toBe(SECRET_FORMAT_HINTS.anthropic);
});
it("returns custom hint for empty custom value", () => {
expect(validateSecretValue("", "custom")).toBe(SECRET_FORMAT_HINTS.custom);
});
});