165 lines
5.5 KiB
TypeScript
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();
|
|
});
|
|
});
|