Initial Commit
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user