import { OpenAPIHono } from "@hono/zod-openapi"; import { createOpenAPIRegistry } from "@suaveplan/api-openapi"; import { createPluginContext } from "@suaveplan/api-plugin"; import { EventBus } from "@suaveplan/events"; import { describe, expect, it } from "@suaveplan/testing/runner"; import { createTenantPlugin } from "../../src/plugin/plugin.js"; import { MemoryTenantStore } from "../../src/store/memory.js"; const noopLogger = { debug: () => undefined, info: () => undefined, warn: () => undefined, error: () => undefined, }; describe("@suaveplan/api-plugin-tenant — integration full boot", () => { it("end-to-end: resolver chain + tenantService + CRUD round-trip", { timeout: 30_000, }, async () => { const store = new MemoryTenantStore(); await store.create({ name: "Seed", slug: "seed", domains: ["seed.example.com"], }); const app = new OpenAPIHono(); const registry = createOpenAPIRegistry({ info: { title: "demo", version: "0.1.0" }, }); const services = new Map(); services.set("app", app); services.set("openAPIRegistry", registry); const plugin = createTenantPlugin({ strategies: [ { type: "header", headerName: "x-tenant-id" }, { type: "query", paramName: "tenant" }, { type: "subdomain", baseDomain: "example.com" }, { type: "domain" }, ], cache: { ttlMs: 1000 }, store, }); const ctx = createPluginContext({ logger: noopLogger, eventBus: new EventBus(), signal: new AbortController().signal, config: plugin.defaults as never, services, }); await plugin.onInit?.(ctx); app.get("/me", (c) => c.json({ tenant: c.get("tenant")?.id ?? null })); // Step 1: list tenants — initially [seed]. const listRes = await app.request("/tenants", { headers: { "x-tenant-id": "tenant_1" }, }); expect(listRes.status).toBe(200); const listBody = (await listRes.json()) as { items: Array<{ slug: string }>; total: number; }; expect(listBody.total).toBe(1); expect(listBody.items[0]?.slug).toBe("seed"); // Step 2: create a new tenant. const createRes = await app.request("/tenants", { method: "POST", headers: { "content-type": "application/json", "x-tenant-id": "tenant_1", }, body: JSON.stringify({ name: "New Tenant", slug: "new", domains: ["new.example.com"], }), }); expect(createRes.status).toBe(200); const created = (await createRes.json()) as { id: string }; // Step 3: read it back. const readRes = await app.request(`/tenants/${created.id}`, { headers: { "x-tenant-id": created.id }, }); expect(readRes.status).toBe(200); // Step 4: resolver chain — header beats query. const r1 = await app.request("/me", { headers: { "x-tenant-id": "tenant_1" }, // seed }); expect(await r1.json()).toEqual({ tenant: "tenant_1" }); // Step 5: query strategy wins when header missing. const r2 = await app.request(`/me?tenant=${created.id}`); expect(await r2.json()).toEqual({ tenant: created.id }); // Step 6: domain strategy via host header. const r3 = await app.request("/me", { headers: { host: "new.example.com" }, }); expect(await r3.json()).toEqual({ tenant: created.id }); // Step 7: cache hit — same key second time. await app.request("/me", { headers: { host: "new.example.com" } }); // Step 8: PATCH updates. const patchRes = await app.request(`/tenants/${created.id}`, { method: "PATCH", headers: { "content-type": "application/json", "x-tenant-id": created.id, }, body: JSON.stringify({ name: "Renamed" }), }); expect(patchRes.status).toBe(200); expect(((await patchRes.json()) as { name: string }).name).toBe( "Renamed" ); // Step 9: DELETE soft-deletes. const delRes = await app.request(`/tenants/${created.id}`, { method: "DELETE", headers: { "x-tenant-id": created.id }, }); expect(delRes.status).toBe(200); const after = await store.getById(created.id); expect(after?.status).toBe("deleted"); }); it("NullTenantContext fallback for unresolved request (required=false)", { timeout: 10_000, }, async () => { const app = new OpenAPIHono(); const registry = createOpenAPIRegistry({ info: { title: "demo", version: "0.1.0" }, }); const services = new Map(); services.set("app", app); services.set("openAPIRegistry", registry); const plugin = createTenantPlugin({ strategies: [{ type: "header", headerName: "x-tenant-id" }], store: new MemoryTenantStore(), }); const ctx = createPluginContext({ logger: noopLogger, eventBus: new EventBus(), signal: new AbortController().signal, config: plugin.defaults as never, services, }); await plugin.onInit?.(ctx); app.get("/me", (c) => c.json({ tenant: c.get("tenant")?.id ?? null })); const res = await app.request("/me"); expect(res.status).toBe(200); expect(await res.json()).toEqual({ tenant: null }); }); });