109 lines
3.3 KiB
TypeScript
109 lines
3.3 KiB
TypeScript
// @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> = {}): 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]",
|
|
})
|
|
);
|
|
});
|
|
});
|