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

165 lines
5.5 KiB
TypeScript

import type {
Principal,
PrincipalId,
Scope,
} from "@suaveplan/api-types/principal";
import type { TenantId } from "@suaveplan/api-types/tenant";
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
JwtTenantResolverError,
TENANT_RESOLVER_JWT_CODES,
} from "../error-codes/error-codes.js";
import { createJwtTenantResolver } from "./resolver.js";
function principalWith(overrides: Partial<Principal> = {}): Principal {
return {
id: "u_1" as PrincipalId,
scopes: [] as readonly Scope[],
claims: {},
...overrides,
};
}
function reqWith(principal: Principal | null): Request {
const req = new Request("https://example.com/x");
Object.defineProperty(req, "principal", {
value: principal,
enumerable: true,
});
return req;
}
describe("createJwtTenantResolver", () => {
it("returns TenantContext when principal carries tenantId", {
timeout: 5000,
}, async () => {
const resolver = createJwtTenantResolver();
const req = reqWith(principalWith({ tenantId: "acme" as TenantId }));
const ctx = await resolver.resolve(req);
expect(ctx).toEqual({ id: "acme", slug: "acme" });
});
it("returns null when principal lacks tenantId (advance chain)", {
timeout: 5000,
}, async () => {
const resolver = createJwtTenantResolver();
const req = reqWith(principalWith());
const ctx = await resolver.resolve(req);
expect(ctx).toBeNull();
});
it("returns null when principal is absent (advance chain)", {
timeout: 5000,
}, async () => {
const resolver = createJwtTenantResolver();
const req = reqWith(null);
const ctx = await resolver.resolve(req);
expect(ctx).toBeNull();
});
it("returns null when default extractor cannot find principal property", {
timeout: 5000,
}, async () => {
const resolver = createJwtTenantResolver();
const req = new Request("https://example.com/y");
const ctx = await resolver.resolve(req);
expect(ctx).toBeNull();
});
it("reads a custom claimKey from Principal.claims", {
timeout: 5000,
}, async () => {
const resolver = createJwtTenantResolver({ claimKey: "tid" });
const req = reqWith(principalWith({ claims: { tid: "acme" } }));
const ctx = await resolver.resolve(req);
expect(ctx).toEqual({ id: "acme", slug: "acme" });
});
it("treats empty-string claim as absent", { timeout: 5000 }, async () => {
const resolver = createJwtTenantResolver({ claimKey: "tid" });
const req = reqWith(principalWith({ claims: { tid: "" } }));
expect(await resolver.resolve(req)).toBeNull();
});
it("treats non-string claim as absent", { timeout: 5000 }, async () => {
const resolver = createJwtTenantResolver({ claimKey: "tid" });
const req = reqWith(principalWith({ claims: { tid: 42 } }));
expect(await resolver.resolve(req)).toBeNull();
});
it("requirePrincipal=true throws when principal is null", {
timeout: 5000,
}, async () => {
const resolver = createJwtTenantResolver({ requirePrincipal: true });
const req = reqWith(null);
await expect(resolver.resolve(req)).rejects.toBeInstanceOf(
JwtTenantResolverError
);
await resolver
.resolve(
reqWith(
principalWith({
tenantId: "ok" as TenantId,
})
)
)
.then((ctx) => expect(ctx).toEqual({ id: "ok", slug: "ok" }));
});
it("requirePrincipal=true throws when claim is missing", {
timeout: 5000,
}, async () => {
const resolver = createJwtTenantResolver({
requirePrincipal: true,
claimKey: "tid",
});
const req = reqWith(principalWith({ claims: {} }));
let thrown: unknown;
try {
await resolver.resolve(req);
} catch (e) {
thrown = e;
}
expect(thrown).toBeInstanceOf(JwtTenantResolverError);
expect((thrown as JwtTenantResolverError).code).toBe(
TENANT_RESOLVER_JWT_CODES.PRINCIPAL_MISSING_TENANT_ID
);
});
it("honours an explicit getPrincipal extractor", {
timeout: 5000,
}, async () => {
const store = new WeakMap<Request, Principal | null>();
const resolver = createJwtTenantResolver({
getPrincipal: (r) => store.get(r) ?? null,
});
const req = new Request("https://example.com/z");
store.set(req, principalWith({ tenantId: "via-map" as TenantId }));
const ctx = await resolver.resolve(req);
expect(ctx).toEqual({ id: "via-map", slug: "via-map" });
});
it("uses the configured id", { timeout: 5000 }, () => {
expect(createJwtTenantResolver().id).toBe("jwt");
expect(createJwtTenantResolver({ id: "primary" }).id).toBe("primary");
});
it("propagates an INVALID_OPTIONS error for bad input", {
timeout: 5000,
}, () => {
expect(() => createJwtTenantResolver({ claimKey: "" })).toThrow(
JwtTenantResolverError
);
});
it("treats undefined principal from extractor as null", {
timeout: 5000,
}, async () => {
const resolver = createJwtTenantResolver({
getPrincipal: () => undefined,
});
const req = new Request("https://example.com/u");
expect(await resolver.resolve(req)).toBeNull();
});
});