import { createTestHost } from "@suaveplan/api-host/testing"; import type { Principal } from "@suaveplan/api-types/principal"; import type { TenantId } from "@suaveplan/api-types/tenant"; import { afterEach, beforeEach, describe, expect, it, } from "@suaveplan/testing/runner"; import { buildRoutes, createInMemoryLedger, EXAMPLE_CODES, EXAMPLE_INVOICE_CREATED_TOPIC, examplePluginModule, USERS_USER_DELETED_TOPIC, } from "../../src/index.js"; import { flushTestMetrics } from "../setup/polyfills.js"; const PRINCIPAL: Principal = { id: "p-int", scopes: ["example:read", "example:write"] as unknown as Principal["scopes"], claims: {}, }; const TENANT_ID = "t-int" as TenantId; let host: Awaited>; let ledger: ReturnType; beforeEach(async () => { ledger = createInMemoryLedger(); host = await createTestHost({ seed: { principal: PRINCIPAL, tenants: [ { id: TENANT_ID, slug: "tenant-int", status: "active", createdAt: new Date("2026-01-01T00:00:00Z").toISOString(), }, ], secrets: { EXAMPLE_STRIPE_KEY: "sk_test_int", EXAMPLE_HMAC_SIGNING_KEY: "hmac_test_int", EXAMPLE_WEBHOOK_TOKEN: "webhook_test_int", }, }, }); await host.installFixturePlugin({ spec: examplePluginModule, routes: buildRoutes({ ledger, publisher: { publish(topic, payload) { host.adapters.eventBus.publish(topic, payload); }, }, }), }); }); afterEach(async () => { await host.dispose(); }); const URL_PREFIX = "http://test.local"; describe("api-plugin-example — full integration flow", () => { it("plugin boots and serves /example/health", { timeout: 30000, }, async () => { const res = await host.fetch( new Request(`${URL_PREFIX}/example/health`) ); expect(res.status).toBe(200); const body = (await res.json()) as { ok: boolean }; expect(body.ok).toBe(true); }); it("REST surface — public bypasses auth", { timeout: 30000 }, async () => { const res = await host.fetchAs( null, new Request(`${URL_PREFIX}/example/public/ping`) ); expect(res.status).toBe(200); const body = (await res.json()) as { ok: boolean }; expect(body.ok).toBe(true); }); it("REST surface — RBAC denies missing scope", { timeout: 30000, }, async () => { const noScope: Principal = { id: "z", scopes: [] as unknown as Principal["scopes"], claims: {}, }; const res = await host.fetchAs( noScope, new Request(`${URL_PREFIX}/example/invoices`) ); expect(res.status).toBe(403); const body = (await res.json()) as { code: string }; expect(body.code).toBe("RBAC_MISSING_SCOPE"); }); it("REST surface — Zod rejects malformed body", { timeout: 30000, }, async () => { const res = await host.fetch( new Request(`${URL_PREFIX}/example/invoices`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ customerId: "c-1" }), }) ); expect(res.status).toBe(400); }); it("REST surface — happy path persists + emits event + increments metric", { timeout: 30000, }, async () => { const eventWait = host.waitForEvent( EXAMPLE_INVOICE_CREATED_TOPIC, 5000 ); const res = await host.fetch( new Request(`${URL_PREFIX}/example/invoices`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ customerId: "c-1", amountCents: 1500, currency: "USD", }), }) ); expect(res.status).toBe(200); const created = (await res.json()) as { id: string; status: string }; expect(created.status).toBe("open"); const event = (await eventWait) as { id: string; amountCents: number }; expect(event.id).toBe(created.id); expect(event.amountCents).toBe(1500); }); it("REST surface — finalise transitions to paid + records OTel metrics", { timeout: 30000, }, async () => { await host.fetch( new Request(`${URL_PREFIX}/example/invoices`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ customerId: "c-1", amountCents: 1, currency: "USD", }), }) ); const finalise = await host.fetch( new Request(`${URL_PREFIX}/example/invoices/inv-1/finalise`, { method: "POST", }) ); expect(finalise.status).toBe(200); const out = (await finalise.json()) as { status: string }; expect(out.status).toBe("paid"); const metricsCollected = await flushTestMetrics(); const allMetrics = metricsCollected.flatMap((rm) => rm.scopeMetrics.flatMap((sm) => sm.metrics) ); const names = new Set(allMetrics.map((m) => m.descriptor.name)); expect(names.has("example.invoices_finalised_total")).toBe(true); expect(names.has("example.stripe.call_duration_ms")).toBe(true); expect(names.has("example.outstanding_invoice_count")).toBe(true); }); it("REST surface — streaming delivers chunked NDJSON", { timeout: 30000, }, async () => { const res = await host.fetch( new Request(`${URL_PREFIX}/example/stream/large-report`) ); expect(res.status).toBe(200); const text = await res.text(); const lines = text.trim().split("\n"); expect(lines.length).toBe(3); }); it("custom error code surfaces with manifest-mapped status", { timeout: 30000, }, async () => { const res = await host.fetch( new Request(`${URL_PREFIX}/example/subscription-required`) ); expect(res.status).toBe(EXAMPLE_CODES.SUBSCRIPTION_INACTIVE ? 409 : 0); expect(res.status).toBe(409); }); it("INVOICE_NOT_FOUND surfaces 404", { timeout: 30000 }, async () => { const res = await host.fetch( new Request(`${URL_PREFIX}/example/invoices/missing`) ); expect(res.status).toBe(404); }); it("INVALID_INVOICE_STATE surfaces 422 on double-finalise", { timeout: 30000, }, async () => { await host.fetch( new Request(`${URL_PREFIX}/example/invoices`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ customerId: "c", amountCents: 1, currency: "USD", }), }) ); await host.fetch( new Request(`${URL_PREFIX}/example/invoices/inv-1/finalise`, { method: "POST", }) ); const res = await host.fetch( new Request(`${URL_PREFIX}/example/invoices/inv-1/finalise`, { method: "POST", }) ); expect(res.status).toBe(422); }); it("subscriber handler runs on incoming users.user.deleted", { timeout: 30000, }, async () => { const sink = { seen: new Set() }; host.adapters.eventBus.on(USERS_USER_DELETED_TOPIC, (raw) => { const p = raw as { userId: string }; sink.seen.add(p.userId); }); host.adapters.eventBus.publish(USERS_USER_DELETED_TOPIC, { userId: "u-1", tenantId: null, }); await host.waitForEvent(USERS_USER_DELETED_TOPIC, 5000); expect(sink.seen.has("u-1")).toBe(true); }); it("every declared secret reads successfully", { timeout: 30000, }, async () => { const secrets = host.adapters.host.secrets; const a = await secrets.require("EXAMPLE_STRIPE_KEY"); const b = await secrets.require("EXAMPLE_HMAC_SIGNING_KEY"); const c = await secrets.require("EXAMPLE_WEBHOOK_TOKEN"); expect(a).toBe("sk_test_int"); expect(b).toBe("hmac_test_int"); expect(c).toBe("webhook_test_int"); }); it("response-validation route returns violating payload with ?failResponse=1", { timeout: 30000, }, async () => { const res = await host.fetch( new Request(`${URL_PREFIX}/_/test/fail-response?failResponse=1`) ); expect(res.status).toBe(200); const body = (await res.json()) as { status: string }; // Intentionally violates the schema — exercises the warn-mode // path. Enforce mode would require host-side responseValidation // wiring which is not yet present in the test-host. expect(body.status).toBe("exploded"); }); it("response-validation route returns valid payload without the flag", { timeout: 30000, }, async () => { const res = await host.fetch( new Request(`${URL_PREFIX}/_/test/fail-response`) ); expect(res.status).toBe(200); const body = (await res.json()) as { status: string }; expect(body.status).toBe("open"); }); });