import { chmod, mkdtemp, readFile, stat } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it, vi } from "vitest"; import { buildDesiredLagoCatalog, type LagoCatalogSnapshot } from "./catalog"; import type { BillingCatalogCommand } from "./contracts"; import type { LagoCatalogClient } from "./lago-client"; import { executeBillingCatalogCommand, writeRedactedCatalogSnapshot } from "./sync"; const desired = buildDesiredLagoCatalog(); const emptySnapshot: LagoCatalogSnapshot = { organization: { name: "HiveGrid Staging", idSha256: "a".repeat(64) }, tax: null, plans: [], }; function command(mode: BillingCatalogCommand["mode"], snapshotDirectory = "tmp/test"): BillingCatalogCommand { return { target: "staging", mode, lagoApiUrl: "https://lago.example.test", lagoApiKey: "secret", expectedOrganizationName: "HiveGrid Staging", expectedOrganizationIdSha256: "a".repeat(64), snapshotDirectory, }; } describe("billing catalog sync", () => { it("plans without mutating and fails verify on drift", async () => { const client = { readCatalog: vi.fn().mockResolvedValue(emptySnapshot), applyTax: vi.fn(), applyPlan: vi.fn(), } as unknown as LagoCatalogClient; const result = await executeBillingCatalogCommand(command("plan"), client); expect(result.complete).toBe(false); expect(client.applyTax).not.toHaveBeenCalled(); await expect(executeBillingCatalogCommand(command("verify"), client)).rejects.toThrow("verification found missing or drifted"); }); it("applies all missing resources and proves convergence", async () => { const directory = await mkdtemp(join(tmpdir(), "hivegrid-billing-catalog-")); const convergedSnapshot: LagoCatalogSnapshot = { organization: emptySnapshot.organization, tax: { code: desired.tax.code, name: desired.tax.name, description: desired.tax.description, rate: desired.tax.rate, applied_to_organization: desired.tax.appliedToOrganization, }, plans: desired.plans.map((plan) => ({ code: plan.code, name: plan.name, invoice_display_name: plan.invoiceDisplayName, description: plan.description, interval: plan.interval, amount_cents: plan.amountCents, amount_currency: plan.amountCurrency, pay_in_advance: plan.payInAdvance, taxes: plan.taxCodes.map((code) => ({ code })), metadata: { ...plan.metadata }, })), }; const client = { readCatalog: vi.fn().mockResolvedValueOnce(emptySnapshot).mockResolvedValueOnce(convergedSnapshot), applyTax: vi.fn(), applyPlan: vi.fn(), } as unknown as LagoCatalogClient; const result = await executeBillingCatalogCommand(command("apply", directory), client); expect(result.complete).toBe(true); expect(client.applyTax).toHaveBeenCalledOnce(); expect(client.applyPlan).toHaveBeenCalledTimes(desired.plans.length); expect(result.snapshotPath).toContain(directory); }); it("writes only redacted catalog state with owner-only permissions", async () => { const directory = await mkdtemp(join(tmpdir(), "hivegrid-billing-snapshot-")); await chmod(directory, 0o700); const path = await writeRedactedCatalogSnapshot({ directory, target: "production", snapshot: emptySnapshot, assessment: { catalogVersion: desired.version, catalogDigestSha256: desired.digestSha256, complete: false, actions: [], }, now: new Date("2026-07-29T08:00:00.000Z"), }); const contents = await readFile(path, "utf8"); expect(contents).not.toContain("secret"); expect(contents).toContain(desired.digestSha256); expect((await stat(path)).mode & 0o777).toBe(0o600); }); });