79 lines
2.7 KiB
TypeScript
79 lines
2.7 KiB
TypeScript
/**
|
|
* Self-conformance for the `TenantResolver` contract: the jwt resolver MUST
|
|
* (a) expose a non-empty `id`, (b) return `Promise<TenantContext | null>`
|
|
* shape on every call, (c) never mutate the inbound `Request`, (d) be safe
|
|
* to call concurrently across many requests sharing the same factory.
|
|
*
|
|
* `@suaveplan/api-types/tenant-resolver` does not (yet) ship a conformance
|
|
* harness; this file stands in as one.
|
|
*/
|
|
import type {
|
|
Principal,
|
|
PrincipalId,
|
|
Scope,
|
|
} from "@suaveplan/api-types/principal";
|
|
import type { TenantId } from "@suaveplan/api-types/tenant";
|
|
import type { TenantResolver } from "@suaveplan/api-types/tenant-resolver";
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import { createJwtTenantResolver } from "../src/resolver/resolver.js";
|
|
|
|
function principal(tenantId: string | undefined): Principal {
|
|
return {
|
|
id: "u" as PrincipalId,
|
|
scopes: [] as readonly Scope[],
|
|
tenantId: tenantId as TenantId | undefined,
|
|
claims: {},
|
|
};
|
|
}
|
|
|
|
function makeRequest(p: Principal | null): Request {
|
|
const req = new Request(`https://example.com/${Math.random()}`);
|
|
Object.defineProperty(req, "principal", { value: p, enumerable: true });
|
|
return req;
|
|
}
|
|
|
|
describe("TenantResolver conformance — jwt", () => {
|
|
const factory = (): TenantResolver => createJwtTenantResolver();
|
|
|
|
it("exposes a non-empty id", { timeout: 5000 }, () => {
|
|
expect(factory().id.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("returns a TenantContext when claim is present", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const ctx = await factory().resolve(makeRequest(principal("t1")));
|
|
expect(ctx).not.toBeNull();
|
|
expect(ctx?.id).toBe("t1");
|
|
expect(typeof ctx?.slug).toBe("string");
|
|
});
|
|
|
|
it("returns null (chain advance) when claim is absent", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
expect(
|
|
await factory().resolve(makeRequest(principal(undefined)))
|
|
).toBeNull();
|
|
});
|
|
|
|
it("does not mutate the inbound Request", { timeout: 5000 }, async () => {
|
|
const req = makeRequest(principal("t2"));
|
|
const before = Object.keys(req).sort();
|
|
await factory().resolve(req);
|
|
expect(Object.keys(req).sort()).toEqual(before);
|
|
});
|
|
|
|
it("is safe to call concurrently", { timeout: 5000 }, async () => {
|
|
const resolver = factory();
|
|
const results = await Promise.all(
|
|
Array.from({ length: 32 }, (_, i) =>
|
|
resolver.resolve(makeRequest(principal(`t${i}`)))
|
|
)
|
|
);
|
|
expect(results).toHaveLength(32);
|
|
for (const [i, ctx] of results.entries()) {
|
|
expect(ctx?.id).toBe(`t${i}`);
|
|
}
|
|
});
|
|
});
|