82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { deriveAuth, isUnauthorized } from "./auth";
|
|
import type { Identity } from "./types";
|
|
|
|
// deriveAuth is the load-bearing auth decision: it decides who is bounced to /login,
|
|
// who is forced through the change-password card, and — critically — who is KEPT in
|
|
// the app despite a /me failure. The one distinction that must never blur is a true
|
|
// 401 (no session → login) versus any other failure (transient → stay functional),
|
|
// because mistaking the latter for the former would log out a healthy Zero-Trust
|
|
// principal on a single flaky request. These cases pin every branch.
|
|
|
|
const admin: Identity = {
|
|
user_id: "u1",
|
|
email: "[email protected]",
|
|
role: "admin",
|
|
is_admin: true,
|
|
must_change_password: false,
|
|
is_owner: false,
|
|
};
|
|
|
|
const err401 = { status: 401, code: "unauthorized", message: "no session" };
|
|
const err500 = { status: 500, code: "error", message: "boom" };
|
|
|
|
describe("isUnauthorized", () => {
|
|
it("is true only for a 401 envelope", () => {
|
|
expect(isUnauthorized(err401)).toBe(true);
|
|
});
|
|
|
|
it("is false for any non-401 failure (transient, 5xx, network)", () => {
|
|
expect(isUnauthorized(err500)).toBe(false);
|
|
expect(isUnauthorized(new TypeError("Failed to fetch"))).toBe(false);
|
|
expect(isUnauthorized(null)).toBe(false);
|
|
expect(isUnauthorized(undefined)).toBe(false);
|
|
expect(isUnauthorized("nope")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("deriveAuth", () => {
|
|
it("while loading: never unauthenticated, never admin, regardless of error", () => {
|
|
const s = deriveAuth(null, err401, true);
|
|
expect(s.loading).toBe(true);
|
|
expect(s.unauthenticated).toBe(false);
|
|
expect(s.isAdmin).toBe(false);
|
|
expect(s.mustChangePassword).toBe(false);
|
|
});
|
|
|
|
it("a settled 401 with no identity is unauthenticated (→ /login)", () => {
|
|
const s = deriveAuth(null, err401, false);
|
|
expect(s.unauthenticated).toBe(true);
|
|
expect(s.isAdmin).toBe(false);
|
|
});
|
|
|
|
it("a settled NON-401 failure is NOT unauthenticated (graded ZT stays functional)", () => {
|
|
const s = deriveAuth(null, err500, false);
|
|
expect(s.unauthenticated).toBe(false);
|
|
// identity is null so admin surfaces stay hidden, but the app keeps rendering.
|
|
expect(s.isAdmin).toBe(false);
|
|
});
|
|
|
|
it("a loaded admin identity is admin and authenticated", () => {
|
|
const s = deriveAuth(admin, null, false);
|
|
expect(s.unauthenticated).toBe(false);
|
|
expect(s.isAdmin).toBe(true);
|
|
expect(s.mustChangePassword).toBe(false);
|
|
});
|
|
|
|
it("surfaces must_change_password from the identity", () => {
|
|
const s = deriveAuth({ ...admin, must_change_password: true }, null, false);
|
|
expect(s.mustChangePassword).toBe(true);
|
|
expect(s.unauthenticated).toBe(false);
|
|
});
|
|
|
|
it("fails closed on a malformed identity missing is_admin / must_change_password", () => {
|
|
// Mirrors the wire-shape trap: absent fields are undefined, not thrown access.
|
|
const partial = { user_id: "u", email: "e", role: "user" } as unknown as Identity;
|
|
const s = deriveAuth(partial, null, false);
|
|
expect(s.isAdmin).toBe(false);
|
|
expect(s.mustChangePassword).toBe(false);
|
|
expect(s.unauthenticated).toBe(false);
|
|
});
|
|
});
|