Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/migrate-api-keys.test.ts
T

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("");
});
});