136 lines
5.1 KiB
TypeScript
136 lines
5.1 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import { runApiKeyMigrationCli } from "./migrate-api-keys.js";
|
|
import type { ApiKeyMigrationCliDependencies } from "./migrate-api-keys.js";
|
|
|
|
function createOutputBuffer(): Pick<typeof process.stdout, "write"> & { text: () => string } {
|
|
const chunks: string[] = [];
|
|
return {
|
|
write: vi.fn((chunk: string | Uint8Array) => {
|
|
chunks.push(String(chunk));
|
|
return true;
|
|
}) as unknown as typeof process.stdout.write,
|
|
text: () => chunks.join(""),
|
|
};
|
|
}
|
|
|
|
function createDependencies(overrides: Partial<ApiKeyMigrationCliDependencies> = {}) {
|
|
const stdout = createOutputBuffer();
|
|
const stderr = createOutputBuffer();
|
|
const backfillLegacyApiKeyHashes = vi.fn().mockResolvedValue({ scanned: 0, updated: 0, skipped: 0, failed: 0, failures: [] });
|
|
const dependencies = {
|
|
stdout,
|
|
stderr,
|
|
backfillLegacyApiKeyHashes,
|
|
...overrides,
|
|
};
|
|
|
|
return dependencies as typeof dependencies & {
|
|
stdout: typeof stdout;
|
|
stderr: typeof stderr;
|
|
backfillLegacyApiKeyHashes: typeof backfillLegacyApiKeyHashes;
|
|
};
|
|
}
|
|
|
|
describe("runApiKeyMigrationCli", () => {
|
|
it("defaults to dry-run and performs no writes", async () => {
|
|
const dependencies = createDependencies();
|
|
|
|
await expect(runApiKeyMigrationCli([], { API_KEY_HASH_SECRET: "fixture-hash-secret" }, dependencies)).resolves.toBe(0);
|
|
|
|
expect(dependencies.backfillLegacyApiKeyHashes).not.toHaveBeenCalled();
|
|
expect(JSON.parse(dependencies.stdout.text())).toMatchObject({
|
|
mode: "dry-run",
|
|
scanned: 0,
|
|
updated: 0,
|
|
skipped: 0,
|
|
failed: 0,
|
|
});
|
|
expect(dependencies.stdout.text()).not.toContain("fixture-hash-secret");
|
|
expect(dependencies.stderr.text()).toBe("");
|
|
});
|
|
|
|
it("executes the backfill with caller batch size and resolved hash secret", async () => {
|
|
const dependencies = createDependencies({
|
|
backfillLegacyApiKeyHashes: vi.fn().mockResolvedValue({
|
|
scanned: 3,
|
|
updated: 2,
|
|
skipped: 1,
|
|
failed: 0,
|
|
failures: [],
|
|
}),
|
|
});
|
|
|
|
await expect(
|
|
runApiKeyMigrationCli(["--execute", "--batch-size", "3"], { API_KEY_HASH_SECRET: "fixture-hash-secret", JWT_SECRET: "fallback-secret" }, dependencies),
|
|
).resolves.toBe(0);
|
|
|
|
expect(dependencies.backfillLegacyApiKeyHashes).toHaveBeenCalledWith({ batchSize: 3, hashSecret: "fixture-hash-secret" });
|
|
expect(JSON.parse(dependencies.stdout.text())).toEqual({
|
|
mode: "execute",
|
|
batchSize: 3,
|
|
scanned: 3,
|
|
updated: 2,
|
|
skipped: 1,
|
|
failed: 0,
|
|
failureIds: [],
|
|
});
|
|
expect(dependencies.stdout.text()).not.toContain("fixture-hash-secret");
|
|
expect(dependencies.stdout.text()).not.toContain("fallback-secret");
|
|
});
|
|
|
|
it("falls back to JWT_SECRET when API_KEY_HASH_SECRET is blank", async () => {
|
|
const dependencies = createDependencies();
|
|
|
|
await expect(runApiKeyMigrationCli(["--execute"], { API_KEY_HASH_SECRET: " ", JWT_SECRET: "jwt-fallback-secret" }, dependencies)).resolves.toBe(0);
|
|
|
|
expect(dependencies.backfillLegacyApiKeyHashes).toHaveBeenCalledWith({ batchSize: 100, hashSecret: "jwt-fallback-secret" });
|
|
expect(dependencies.stdout.text()).not.toContain("jwt-fallback-secret");
|
|
});
|
|
|
|
it("rejects invalid batch-size before DB writes", async () => {
|
|
const dependencies = createDependencies();
|
|
|
|
await expect(runApiKeyMigrationCli(["--execute", "--batch-size", "0"], { API_KEY_HASH_SECRET: "fixture-hash-secret" }, dependencies)).resolves.toBe(1);
|
|
|
|
expect(dependencies.backfillLegacyApiKeyHashes).not.toHaveBeenCalled();
|
|
expect(dependencies.stderr.text()).toContain("Invalid --batch-size");
|
|
expect(dependencies.stderr.text()).not.toContain("fixture-hash-secret");
|
|
expect(dependencies.stdout.text()).toBe("");
|
|
});
|
|
|
|
it("redacts raw fixture secrets from failed summaries", async () => {
|
|
const rawKey = "api_test_plaintext_secret";
|
|
const dependencies = createDependencies({
|
|
backfillLegacyApiKeyHashes: vi.fn().mockResolvedValue({
|
|
scanned: 1,
|
|
updated: 0,
|
|
skipped: 0,
|
|
failed: 1,
|
|
failures: [{ id: "api_key_1", error: `database leaked ${rawKey}` }],
|
|
}),
|
|
});
|
|
|
|
await expect(runApiKeyMigrationCli(["--execute"], { JWT_SECRET: "jwt-fallback-secret" }, dependencies)).resolves.toBe(1);
|
|
|
|
expect(dependencies.stdout.text()).toContain("api_key_1");
|
|
expect(dependencies.stdout.text()).not.toContain(rawKey);
|
|
expect(dependencies.stdout.text()).not.toContain("jwt-fallback-secret");
|
|
expect(dependencies.stderr.text()).toBe("");
|
|
});
|
|
|
|
it("redacts thrown migration helper errors", async () => {
|
|
const rawKey = "api_test_plaintext_secret";
|
|
const dependencies = createDependencies({
|
|
backfillLegacyApiKeyHashes: vi.fn().mockRejectedValue(new Error(`connection failed for ${rawKey}`)),
|
|
});
|
|
|
|
await expect(runApiKeyMigrationCli(["--execute"], { API_KEY_HASH_SECRET: "fixture-hash-secret" }, dependencies)).resolves.toBe(1);
|
|
|
|
expect(dependencies.stderr.text()).toContain("Error");
|
|
expect(dependencies.stderr.text()).not.toContain(rawKey);
|
|
expect(dependencies.stderr.text()).not.toContain("fixture-hash-secret");
|
|
expect(dependencies.stdout.text()).toBe("");
|
|
});
|
|
});
|