152 lines
4.8 KiB
TypeScript
152 lines
4.8 KiB
TypeScript
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> = {}): 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<string, Tenant>([
|
|
["acme", makeTenant("acme")],
|
|
["beta", makeTenant("beta")],
|
|
]);
|
|
let dbCalls = 0;
|
|
const lookup = async (k: string): Promise<Tenant | null> => {
|
|
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<string, Tenant>([["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);
|
|
});
|
|
});
|