import { describe, expect, it } from "@suaveplan/testing/runner"; import { AUTH_BA_CODES, BetterAuthError } from "../error-codes/error-codes.js"; import { mapBetterAuthUserToPrincipal } from "./map-user-to-principal.js"; describe("mapBetterAuthUserToPrincipal", () => { it("maps a minimal user", { timeout: 5000 }, () => { const p = mapBetterAuthUserToPrincipal({ user: { id: "u_1" }, session: {}, }); expect(p.id).toBe("u_1" as never); expect(p.scopes).toEqual([]); expect(p.claims).toEqual({ id: "u_1" }); expect(p.tenantId).toBeUndefined(); }); it("translates roles into branded scopes", { timeout: 5000 }, () => { const p = mapBetterAuthUserToPrincipal({ user: { id: "u_1", roles: ["admin", "billing:read", ""] }, session: {}, }); // Empty role string is stripped. expect(p.scopes).toEqual(["admin", "billing:read"]); }); it("prefers session.activeOrganizationId over user.tenantId", { timeout: 5000, }, () => { const p = mapBetterAuthUserToPrincipal({ user: { id: "u_1", tenantId: "user-tenant" }, session: { activeOrganizationId: "session-org" }, }); expect(p.tenantId).toBe("session-org" as never); }); it("falls back to user.tenantId when session has no org", { timeout: 5000, }, () => { const p = mapBetterAuthUserToPrincipal({ user: { id: "u_1", tenantId: "user-tenant" }, session: {}, }); expect(p.tenantId).toBe("user-tenant" as never); }); it("drops sensitive fields from claims", { timeout: 5000 }, () => { const p = mapBetterAuthUserToPrincipal({ user: { id: "u_1", email: "x@y.z", passwordHash: "$argon2id$...", password: "leak", salt: "deadbeef", magicLinkToken: "tok", resetPasswordToken: "rst", twoFactorSecret: "secret", }, session: {}, }); const claimKeys = Object.keys(p.claims); expect(claimKeys).toContain("email"); expect(claimKeys).not.toContain("passwordHash"); expect(claimKeys).not.toContain("password"); expect(claimKeys).not.toContain("salt"); expect(claimKeys).not.toContain("magicLinkToken"); expect(claimKeys).not.toContain("resetPasswordToken"); expect(claimKeys).not.toContain("twoFactorSecret"); }); it("attaches session_id to claims when present", { timeout: 5000 }, () => { const p = mapBetterAuthUserToPrincipal({ user: { id: "u_1" }, session: { id: "sess_xxx" }, }); expect(p.claims["session_id"]).toBe("sess_xxx"); }); it("ignores empty session id", { timeout: 5000 }, () => { const p = mapBetterAuthUserToPrincipal({ user: { id: "u_1" }, session: { id: "" }, }); expect(p.claims["session_id"]).toBeUndefined(); }); it("ignores undefined user fields when building claims", { timeout: 5000, }, () => { const p = mapBetterAuthUserToPrincipal({ user: { id: "u_1", email: undefined }, session: {}, }); expect(p.claims["email"]).toBeUndefined(); }); it("treats non-string roles as no roles", { timeout: 5000 }, () => { const p = mapBetterAuthUserToPrincipal({ user: { id: "u_1", roles: null }, session: {}, }); expect(p.scopes).toEqual([]); const p2 = mapBetterAuthUserToPrincipal({ user: { id: "u_1", roles: undefined }, session: {}, }); expect(p2.scopes).toEqual([]); }); it("skips non-string roles inside the array", { timeout: 5000 }, () => { const p = mapBetterAuthUserToPrincipal({ user: { id: "u_1", roles: ["admin", 5 as never, "ops"] }, session: {}, }); expect(p.scopes).toEqual(["admin", "ops"]); }); it("treats empty tenantId strings as absent on both sides", { timeout: 5000, }, () => { const p = mapBetterAuthUserToPrincipal({ user: { id: "u_1", tenantId: "" }, session: { activeOrganizationId: "" }, }); expect(p.tenantId).toBeUndefined(); }); it("throws AUTH_BA.PRINCIPAL_MAPPING_FAILED when id is missing", { timeout: 5000, }, () => { try { mapBetterAuthUserToPrincipal({ // Force missing-id at the runtime boundary even though the // type contract requires it — the mapper MUST guard. user: { id: undefined as never }, session: {}, }); expect.fail("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(BetterAuthError); expect((err as BetterAuthError).code).toBe( AUTH_BA_CODES.PRINCIPAL_MAPPING_FAILED ); } }); it("throws when id is an empty string", { timeout: 5000 }, () => { expect(() => mapBetterAuthUserToPrincipal({ user: { id: "" }, session: {}, }) ).toThrow(BetterAuthError); }); it("freezes the returned principal and its claims", { timeout: 5000, }, () => { const p = mapBetterAuthUserToPrincipal({ user: { id: "u_1" }, session: { id: "s_1" }, }); expect(Object.isFrozen(p)).toBe(true); expect(Object.isFrozen(p.claims)).toBe(true); }); });