import { describe, expect, it } from "@suaveplan/testing/runner"; import { AUTH_JWT_CODES, type AuthJwtError, } from "../error-codes/error-codes.js"; import { defaultClaimsToPrincipal, runClaimsMapper } from "./claims-mapper.js"; describe("defaultClaimsToPrincipal", () => { it("projects sub + scope + tenant_id", { timeout: 5000 }, () => { const p = defaultClaimsToPrincipal({ sub: "user_1", scope: "billing:read profile:read", tenant_id: "acme", }); expect(p?.id).toBe("user_1"); expect(p?.scopes).toEqual(["billing:read", "profile:read"]); expect(p?.tenantId).toBe("acme"); }); it("supports array-form scopes", { timeout: 5000 }, () => { const p = defaultClaimsToPrincipal({ sub: "user_2", scopes: ["a", "b"], }); expect(p?.scopes).toEqual(["a", "b"]); expect(p?.tenantId).toBeUndefined(); }); it("falls back to `tid` claim for tenant", { timeout: 5000 }, () => { const p = defaultClaimsToPrincipal({ sub: "user_3", tid: "ms-tenant", }); expect(p?.tenantId).toBe("ms-tenant"); }); it("returns null when sub missing", { timeout: 5000 }, () => { expect(defaultClaimsToPrincipal({})).toBeNull(); expect(defaultClaimsToPrincipal({ sub: "" })).toBeNull(); expect(defaultClaimsToPrincipal({ sub: 123 as never })).toBeNull(); }); it("filters empty scope segments", { timeout: 5000 }, () => { const p = defaultClaimsToPrincipal({ sub: "u", scope: "a b", }); expect(p?.scopes).toEqual(["a", "b"]); }); it("ignores non-string scopes array entries", { timeout: 5000 }, () => { const p = defaultClaimsToPrincipal({ sub: "u", scopes: ["a", 2 as never, "b"], }); expect(p?.scopes).toEqual(["a", "b"]); }); it("ignores non-string scope claim", { timeout: 5000 }, () => { const p = defaultClaimsToPrincipal({ sub: "u", scope: 7 as never, }); expect(p?.scopes).toEqual([]); }); it("ignores non-string tenant_id", { timeout: 5000 }, () => { const p = defaultClaimsToPrincipal({ sub: "u", tenant_id: 7 as never, }); expect(p?.tenantId).toBeUndefined(); }); }); describe("runClaimsMapper", () => { it("passes through valid Principal", { timeout: 5000 }, () => { const p = runClaimsMapper(defaultClaimsToPrincipal, { sub: "u" }); expect(p?.id).toBe("u"); }); it("propagates null", { timeout: 5000 }, () => { expect(runClaimsMapper(() => null, { sub: "u" })).toBeNull(); }); it("throws AUTH_JWT.INVALID_CLAIMS_MAPPER when mapper throws", { timeout: 5000, }, () => { try { runClaimsMapper( () => { throw new Error("boom"); }, { sub: "u" } ); throw new Error("expected throw"); } catch (err) { expect((err as AuthJwtError).code).toBe( AUTH_JWT_CODES.INVALID_CLAIMS_MAPPER ); } }); it("throws when mapper returns non-Principal", { timeout: 5000 }, () => { try { runClaimsMapper(() => ({ bogus: 1 }) as never, { sub: "u" }); throw new Error("expected throw"); } catch (err) { expect((err as AuthJwtError).code).toBe( AUTH_JWT_CODES.INVALID_CLAIMS_MAPPER ); } }); it("rejects mapper returning a non-object", { timeout: 5000 }, () => { try { runClaimsMapper(() => "u" as never, { sub: "u" }); throw new Error("expected throw"); } catch (err) { expect((err as AuthJwtError).code).toBe( AUTH_JWT_CODES.INVALID_CLAIMS_MAPPER ); } }); it("rejects mapper returning Principal-shape with null claims", { timeout: 5000, }, () => { try { runClaimsMapper( () => ({ id: "u", scopes: [], claims: null, }) as never, { sub: "u" } ); throw new Error("expected throw"); } catch (err) { expect((err as AuthJwtError).code).toBe( AUTH_JWT_CODES.INVALID_CLAIMS_MAPPER ); } }); it("rejects mapper missing string id", { timeout: 5000 }, () => { try { runClaimsMapper( () => ({ id: 1, scopes: [], claims: {} }) as never, { sub: "u" } ); throw new Error("expected throw"); } catch (err) { expect((err as AuthJwtError).code).toBe( AUTH_JWT_CODES.INVALID_CLAIMS_MAPPER ); } }); it("rejects mapper with non-array scopes", { timeout: 5000 }, () => { try { runClaimsMapper( () => ({ id: "u", scopes: "a", claims: {} }) as never, { sub: "u" } ); throw new Error("expected throw"); } catch (err) { expect((err as AuthJwtError).code).toBe( AUTH_JWT_CODES.INVALID_CLAIMS_MAPPER ); } }); });