Files
suaveplan/packages/api-stack/api/api-tenant/src/storage/storage.test.ts
2026-05-27 23:29:53 +01:00

138 lines
4.1 KiB
TypeScript

import { afterEach, describe, expect, it } from "@suaveplan/testing/runner";
import { Hono } from "hono";
import { createTenantMiddleware } from "../middleware/middleware.js";
import { headerResolver } from "../resolver/resolver.js";
import type { Tenant } from "../types/tenant.js";
import {
getCurrentTenant,
getCurrentTenantId,
isTenantStorageEnabled,
runWithTenant,
tenantStorageMiddleware,
withTenantStorage,
} from "./storage.js";
function makeTenant(id = "t_1"): Tenant {
return {
id,
name: "Acme",
slug: id,
status: "active",
settings: {},
enabledPlugins: [],
domains: [],
createdAt: new Date(0),
updatedAt: new Date(0),
};
}
afterEach(() => {
withTenantStorage(false);
});
describe("withTenantStorage toggle", () => {
it("defaults to off", { timeout: 5000 }, () => {
expect(isTenantStorageEnabled()).toBe(false);
});
it("toggles on/off", { timeout: 5000 }, () => {
withTenantStorage(true);
expect(isTenantStorageEnabled()).toBe(true);
withTenantStorage(false);
expect(isTenantStorageEnabled()).toBe(false);
});
});
describe("AsyncLocalStorage thread", () => {
it("ALS off keeps getCurrentTenant undefined", {
timeout: 5000,
}, async () => {
const app = new Hono();
let seen: Tenant | undefined;
app.use(
"*",
createTenantMiddleware({
resolver: headerResolver("x-tenant-id", async () =>
makeTenant()
),
})
);
app.use("*", tenantStorageMiddleware());
app.get("/", async (c) => {
await new Promise<void>((r) => setImmediate(r));
seen = getCurrentTenant();
return c.text("ok");
});
await app.request("/", { headers: { "x-tenant-id": "acme" } });
expect(seen).toBeUndefined();
});
it("ALS on threads tenant through async stack", {
timeout: 5000,
}, async () => {
withTenantStorage(true);
const app = new Hono();
let seenId: string | undefined;
app.use(
"*",
createTenantMiddleware({
resolver: headerResolver("x-tenant-id", async () =>
makeTenant("acme-deep")
),
})
);
app.use("*", tenantStorageMiddleware());
app.get("/", async (c) => {
// Cross several async boundaries before reading.
await new Promise<void>((r) => setImmediate(r));
await new Promise<void>((r) => setTimeout(r, 1));
seenId = getCurrentTenantId();
return c.text("ok");
});
await app.request("/", { headers: { "x-tenant-id": "acme" } });
expect(seenId).toBe("acme-deep");
});
it("middleware is no-op when tenant unresolved (ALS on)", {
timeout: 5000,
}, async () => {
withTenantStorage(true);
const app = new Hono();
app.use("*", tenantStorageMiddleware());
app.get("/", (c) => {
expect(getCurrentTenant()).toBeUndefined();
return c.text("ok");
});
const res = await app.request("/");
expect(res.status).toBe(200);
});
it("runWithTenant runs inside ALS scope", {
timeout: 5000,
}, async () => {
withTenantStorage(true);
const t = makeTenant("explicit");
const id = runWithTenant(t, () => getCurrentTenantId());
expect(id).toBe("explicit");
});
it("runWithTenant runs fn directly when ALS off", {
timeout: 5000,
}, () => {
const t = makeTenant();
const id = runWithTenant(t, () => getCurrentTenantId());
expect(id).toBeUndefined();
});
it("middleware bypasses ALS when toggle is off", {
timeout: 5000,
}, async () => {
const app = new Hono();
app.use("*", tenantStorageMiddleware());
app.get("/", (c) => c.text("ok"));
const res = await app.request("/");
expect(res.status).toBe(200);
expect(isTenantStorageEnabled()).toBe(false);
});
});