// @vitest-environment node import { describe, expect, test } from "vitest"; import type { ApiKeyWithDetails } from "./apiKeys.db.server"; import { getApiKeyLookupCacheKey, serializeApiLogSearchParams } from "../api/apiKeySecurity.server"; import { redactApiKeyForDisplay, redactApiKeysForDisplay } from "./apiKeys.db.server"; const RAW_SECRET = "raw-secret-value-that-must-not-leak"; function createApiKeyRow(overrides: Partial = {}): ApiKeyWithDetails { const now = new Date("2026-01-02T03:04:05.000Z"); return { id: "api-key-1", tenantId: "tenant-1", createdByUserId: "user-1", alias: "Primary integration", key: RAW_SECRET, keyHash: "hash-value", keyPrefix: "raw-secr", keyLastFour: "leak", hashedAt: now, keyVersion: 1, expires: null, active: true, createdAt: now, tenant: { id: "tenant-1", name: "Tenant", slug: "tenant", deactivatedReason: null }, createdByUser: { id: "user-1", email: "user@example.com", firstName: "Ada", lastName: "Lovelace", phone: null, locale: "en", createdAt: now, }, entities: [ { id: "permission-1", apiKeyId: "api-key-1", entityId: "entity-1", create: true, read: true, update: false, delete: false, entity: { id: "entity-1", title: "Contact", titlePlural: "Contacts", slug: "contacts" }, }, ], _count: { apiKeyLogs: 7 }, ...overrides, }; } describe("API key display redaction", () => { test("omits raw key while preserving display metadata for detail payloads", () => { const source = createApiKeyRow(); const redacted = redactApiKeyForDisplay(source); expect(redacted).not.toHaveProperty("key"); expect(redacted).toMatchObject({ id: source.id, alias: source.alias, active: source.active, expires: source.expires, keyPrefix: source.keyPrefix, keyLastFour: source.keyLastFour, hashedAt: source.hashedAt, keyVersion: source.keyVersion, entities: source.entities, createdByUser: source.createdByUser, tenant: source.tenant, _count: source._count, }); expect(JSON.stringify(redacted)).not.toContain(RAW_SECRET); }); test("redacts list payloads without mutating source rows", () => { const source = createApiKeyRow(); const redacted = redactApiKeysForDisplay([source]); expect(redacted).toHaveLength(1); expect(redacted[0]).not.toHaveProperty("key"); expect(JSON.stringify(redacted)).not.toContain(RAW_SECRET); expect(source.key).toBe(RAW_SECRET); expect(source).toHaveProperty("key", RAW_SECRET); }); test("builds API-key cache keys without embedding raw secrets", () => { const cacheKey = getApiKeyLookupCacheKey(RAW_SECRET); expect(cacheKey).toMatch(/^apiKeyHash:[a-f0-9]{64}$/); expect(cacheKey).not.toContain(RAW_SECRET); }); test("redacts query params for API-key log writers", () => { const request = new Request("https://example.com/api/site?token=raw-token&email=person@example.test&page=2&apiKey=sk_live_secret"); expect(serializeApiLogSearchParams(request)).toBe( JSON.stringify({ token: "[REDACTED]", email: "[REDACTED]", page: "2", apiKey: "[REDACTED]", }) ); }); });