Initial Commit
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
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<string, unknown>();
|
||||
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<string, unknown>();
|
||||
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 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user