import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { createAPIKey, createTenant, fetchAPIKeys, fetchAudit, fetchCatalog, fetchEntitlements, fetchTenantBySlug, requestProduct, revokeAPIKey, startTrial, type Tenant, } from "./tenant-registry"; const SAMPLE: Tenant = { id: "00000000-0000-0000-0000-000000000001", slug: "acme", name: "Acme Inc.", status: "active", kind: "customer", plan: "professional", created_at: "2026-05-18T22:00:00Z", updated_at: "2026-05-18T22:00:00Z", }; const originalFetch = globalThis.fetch; const originalRegistryUrl = process.env.TENANT_REGISTRY_URL; afterEach(() => { globalThis.fetch = originalFetch; process.env.TENANT_REGISTRY_URL = originalRegistryUrl; vi.restoreAllMocks(); }); beforeEach(() => { process.env.TENANT_REGISTRY_URL = "http://test:1234"; }); function mockJSON(status: number, body: unknown) { return vi.fn(async () => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" }, }), ); } describe("fetchTenantBySlug", () => { test("200 → parsed tenant", async () => { globalThis.fetch = mockJSON(200, SAMPLE); expect(await fetchTenantBySlug("acme")).toEqual(SAMPLE); }); test("404 → null", async () => { globalThis.fetch = mockJSON(404, {}); expect(await fetchTenantBySlug("nope")).toBeNull(); }); test("500 → throws", async () => { globalThis.fetch = mockJSON(500, {}); await expect(fetchTenantBySlug("acme")).rejects.toThrow(/500/); }); test("default base URL", async () => { delete process.env.TENANT_REGISTRY_URL; const spy = mockJSON(200, SAMPLE); globalThis.fetch = spy; await fetchTenantBySlug("acme"); expect(spy.mock.calls[0]![0]).toBe("http://localhost:8090/v1/tenants/by-slug/acme"); }); test("encodes slug", async () => { const spy = mockJSON(404, {}); globalThis.fetch = spy; await fetchTenantBySlug("a/b c"); expect(spy.mock.calls[0]![0]).toBe("http://test:1234/v1/tenants/by-slug/a%2Fb%20c"); }); }); describe("fetchCatalog", () => { test("returns items[]", async () => { globalThis.fetch = mockJSON(200, { items: [ { key: "certifai", name: "CERTifAI", description: "x", plans_required: [], supports_trial: true }, ], }); const list = await fetchCatalog(); expect(list).toHaveLength(1); expect(list[0].key).toBe("certifai"); }); test("non-200 throws", async () => { globalThis.fetch = mockJSON(500, {}); await expect(fetchCatalog()).rejects.toThrow(); }); }); describe("fetchEntitlements", () => { test("happy path", async () => { globalThis.fetch = mockJSON(200, { items: [{ tenant_id: "t1", product: "certifai", enabled: true, config: {} }], }); expect(await fetchEntitlements("t1")).toHaveLength(1); }); test("404 → []", async () => { globalThis.fetch = mockJSON(404, {}); expect(await fetchEntitlements("t1")).toEqual([]); }); }); describe("requestProduct", () => { test("202 → ok", async () => { globalThis.fetch = mockJSON(202, { status: "accepted" }); expect(await requestProduct("t1", "certifai")).toEqual({ ok: true }); }); test("404 maps to tenant_not_found", async () => { globalThis.fetch = mockJSON(404, {}); expect(await requestProduct("t1", "certifai")).toEqual({ ok: false, error: "tenant_not_found", }); }); test("400 maps to invalid_input", async () => { globalThis.fetch = mockJSON(400, {}); expect(await requestProduct("t1", "x")).toEqual({ ok: false, error: "invalid_input" }); }); test("unexpected status surfaces with code", async () => { globalThis.fetch = mockJSON(503, {}); expect(await requestProduct("t1", "x")).toEqual({ ok: false, error: "unexpected_503" }); }); }); describe("startTrial", () => { test("201 → entitlement", async () => { globalThis.fetch = mockJSON(201, { tenant_id: "t1", product: "certifai", enabled: true, config: { source: "trial" }, }); const res = await startTrial("t1", "certifai"); expect(res.ok).toBe(true); if (res.ok) expect(res.entitlement.product).toBe("certifai"); }); test("400 maps to invalid_input", async () => { globalThis.fetch = mockJSON(400, {}); expect(await startTrial("t1", "x")).toEqual({ ok: false, error: "invalid_input" }); }); }); describe("createTenant", () => { test("201 returns tenant", async () => { globalThis.fetch = mockJSON(201, { tenant: SAMPLE, invite_url: "http://mock/invite", }); const res = await createTenant({ slug: "x", name: "X", admin_email: "a@b.test" }); expect(res.ok).toBe(true); if (res.ok) { expect(res.tenant.slug).toBe("acme"); expect(res.invite_url).toBe("http://mock/invite"); } }); test("409 maps to slug_taken", async () => { globalThis.fetch = mockJSON(409, {}); expect(await createTenant({ slug: "x", name: "X" })).toEqual({ ok: false, error: "slug_taken", }); }); }); describe("coverage gaps", () => { test("startTrial 404 maps to tenant_not_found", async () => { globalThis.fetch = mockJSON(404, {}); expect(await startTrial("t1", "x")).toEqual({ ok: false, error: "tenant_not_found", }); }); test("startTrial unexpected status surfaces with code", async () => { globalThis.fetch = mockJSON(503, {}); expect(await startTrial("t1", "x")).toEqual({ ok: false, error: "unexpected_503" }); }); test("createTenant 400 maps to invalid_input", async () => { globalThis.fetch = mockJSON(400, {}); expect(await createTenant({ slug: "x", name: "X" })).toEqual({ ok: false, error: "invalid_input", }); }); test("createTenant unexpected status surfaces with code", async () => { globalThis.fetch = mockJSON(500, {}); expect(await createTenant({ slug: "x", name: "X" })).toEqual({ ok: false, error: "unexpected_500", }); }); test("req() handles 204 with null data", async () => { // Use a verb that returns 204 — none of our endpoints do, but make sure // the helper handles it. Simulate via fetchEntitlements with 204. globalThis.fetch = vi.fn(async () => new Response(null, { status: 204 })); await expect(fetchEntitlements("t1")).rejects.toThrow(); }); test("fetchCatalog with no data throws", async () => { globalThis.fetch = vi.fn(async () => new Response("not-json", { status: 200, headers: { "content-type": "text/plain" } }), ); await expect(fetchCatalog()).rejects.toThrow(); }); }); describe("fetchAPIKeys", () => { test("happy path", async () => { globalThis.fetch = mockJSON(200, { items: [ { id: "1", tenant_id: "t1", name: "k1", scopes: [], prefix: "bp_a", created_at: "x" }, ], }); const list = await fetchAPIKeys("t1"); expect(list).toHaveLength(1); }); test("404 → []", async () => { globalThis.fetch = mockJSON(404, {}); expect(await fetchAPIKeys("t1")).toEqual([]); }); test("non-200 throws", async () => { globalThis.fetch = mockJSON(500, {}); await expect(fetchAPIKeys("t1")).rejects.toThrow(); }); }); describe("createAPIKey", () => { test("201 returns plaintext", async () => { globalThis.fetch = mockJSON(201, { api_key: { id: "1", tenant_id: "t1", name: "k", scopes: [], prefix: "bp_a", created_at: "x" }, plaintext: "bp_abc123", }); const res = await createAPIKey({ tenant_id: "t1", name: "k" }); expect(res.ok).toBe(true); if (res.ok) expect(res.plaintext).toBe("bp_abc123"); }); test("404 → tenant_not_found", async () => { globalThis.fetch = mockJSON(404, {}); expect(await createAPIKey({ tenant_id: "t1", name: "k" })).toEqual({ ok: false, error: "tenant_not_found", }); }); test("400 → invalid_input", async () => { globalThis.fetch = mockJSON(400, {}); expect(await createAPIKey({ tenant_id: "t1", name: "" })).toEqual({ ok: false, error: "invalid_input", }); }); test("409 → name_taken", async () => { globalThis.fetch = mockJSON(409, {}); expect(await createAPIKey({ tenant_id: "t1", name: "k" })).toEqual({ ok: false, error: "name_taken", }); }); test("unexpected status", async () => { globalThis.fetch = mockJSON(500, {}); expect(await createAPIKey({ tenant_id: "t1", name: "k" })).toEqual({ ok: false, error: "unexpected_500", }); }); }); describe("revokeAPIKey", () => { test("204 → ok", async () => { globalThis.fetch = vi.fn(async () => new Response(null, { status: 204 })); expect(await revokeAPIKey("k1")).toEqual({ ok: true }); }); test("404 → not_found", async () => { globalThis.fetch = mockJSON(404, {}); expect(await revokeAPIKey("k1")).toEqual({ ok: false, error: "not_found" }); }); test("unexpected status", async () => { globalThis.fetch = mockJSON(500, {}); expect(await revokeAPIKey("k1")).toEqual({ ok: false, error: "unexpected_500" }); }); }); describe("fetchAudit", () => { test("happy path with filters", async () => { const spy = mockJSON(200, { items: [{ id: 1, action: "tenant.created", created_at: "x" }], next_cursor: 1, }); globalThis.fetch = spy; const res = await fetchAudit({ tenant_id: "t1", product: "certifai", actor_id: "u1", action: "tenant.created", since: "2026-05-01T00:00:00Z", until: "2026-05-20T00:00:00Z", limit: 50, cursor: 10, }); expect(res.items).toHaveLength(1); expect(res.next_cursor).toBe(1); const url = String(spy.mock.calls[0]![0]); expect(url).toContain("tenant_id=t1"); expect(url).toContain("product=certifai"); expect(url).toContain("actor_id=u1"); expect(url).toContain("action=tenant.created"); expect(url).toContain("limit=50"); expect(url).toContain("cursor=10"); }); test("no filters", async () => { const spy = mockJSON(200, { items: [] }); globalThis.fetch = spy; const res = await fetchAudit({}); expect(res.items).toEqual([]); const url = String(spy.mock.calls[0]![0]); expect(url).toBe("http://test:1234/v1/audit?"); }); test("non-200 throws", async () => { globalThis.fetch = mockJSON(500, {}); await expect(fetchAudit({ tenant_id: "t1" })).rejects.toThrow(); }); });