102 lines
3.8 KiB
TypeScript
102 lines
3.8 KiB
TypeScript
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);
|
|
});
|
|
});
|