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