import { describe, expect, it } from "@suaveplan/testing/runner"; import { Hono } from "hono"; import { chainResolvers, createTenantMiddleware, headerResolver, MemoryRateLimitStore, subdomainResolver, TENANT_CODES, type Tenant, TenantCache, TenantError, tenantRateLimit, } from "../../src/index.js"; function makeTenant(id: string, over: Partial = {}): Tenant { return { id, name: id, slug: id, status: "active", settings: {}, enabledPlugins: [], domains: [], createdAt: new Date(0), updatedAt: new Date(0), ...over, }; } describe("integration — tenant middleware roundtrip (real Hono + cache + store)", () => { it("resolves via chain, caches positive lookup, enforces rate limit", { timeout: 30000, }, async () => { const db = new Map([ ["acme", makeTenant("acme")], ["beta", makeTenant("beta")], ]); let dbCalls = 0; const lookup = async (k: string): Promise => { dbCalls += 1; return db.get(k) ?? null; }; const resolver = TenantCache.wrap( chainResolvers( headerResolver("x-tenant-id", lookup), subdomainResolver("app.test", lookup) ), { ttlMs: 60_000 } ); const store = new MemoryRateLimitStore(100); const app = new Hono(); app.use("*", createTenantMiddleware({ resolver })); app.use( "*", tenantRateLimit({ maxRequests: 2, windowMs: 60_000, store, keyPrefix: "rl", }) ); app.get("/me", (c) => c.json({ id: c.get("tenant").id })); app.onError((err, c) => { if (err instanceof TenantError) { return c.json({ code: err.code }, 404); } return c.text("boom", 500); }); // 1. First hit — cold cache, db touched. const r1 = await app.request("/me", { headers: { "x-tenant-id": "acme" }, }); expect(r1.status).toBe(200); expect(dbCalls).toBe(1); // 2. Second hit — cache hit, db untouched. const r2 = await app.request("/me", { headers: { "x-tenant-id": "acme" }, }); expect(r2.status).toBe(200); expect(dbCalls).toBe(1); // 3. Third hit — rate-limit exceeded. const r3 = await app.request("/me", { headers: { "x-tenant-id": "acme" }, }); expect(r3.status).toBe(429); const body = (await r3.json()) as { code: string }; expect(body.code).toBe(TENANT_CODES.RATE_LIMITED); expect(r3.headers.get("Retry-After")).not.toBeNull(); }); it("subdomain fallback resolves when header missing", { timeout: 30000, }, async () => { const db = new Map([["sub", makeTenant("sub")]]); const resolver = TenantCache.wrap( chainResolvers( headerResolver("x-tenant-id", async (k) => db.get(k) ?? null), subdomainResolver("app.test", async (k) => db.get(k) ?? null) ), { ttlMs: 60_000, negativeTtlMs: 1000 } ); const app = new Hono(); app.use("*", createTenantMiddleware({ resolver })); app.get("/", (c) => c.text(c.get("tenant").id)); app.onError((err, c) => c.json( { code: err instanceof TenantError ? err.code : "x", message: err.message, }, 500 ) ); const res = await app.request("http://sub.app.test/", { headers: { host: "sub.app.test" }, }); expect(await res.text()).toBe("sub"); }); it("unknown tenant in chain → 404 via onError mapping", { timeout: 30000, }, async () => { const resolver = TenantCache.wrap( chainResolvers( headerResolver("x-tenant-id", async () => null), subdomainResolver("app.test", async () => null) ), { ttlMs: 60_000 } ); const app = new Hono(); app.use("*", createTenantMiddleware({ resolver })); app.get("/", (c) => c.text(c.get("tenant").id)); app.onError((err, c) => { if (err instanceof TenantError) return c.json({ code: err.code }, 404); return c.text("", 500); }); const res = await app.request("http://unknown.app.test/", { headers: { host: "unknown.app.test" }, }); expect(res.status).toBe(404); const body = (await res.json()) as { code: string }; expect(body.code).toBe(TENANT_CODES.NOT_FOUND); }); });