import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { createRequire } from "node:module"; import test from "node:test"; import vm from "node:vm"; const require = createRequire(import.meta.url); const { transformSync } = require("esbuild"); const source = readFileSync(new URL("../src/App.tsx", import.meta.url), "utf8"); const code = transformSync(source, { loader: "tsx", format: "cjs", target: "es2022" }).code; const context = vm.createContext({ module: { exports: {} }, require: (name) => name === "react" ? { lazy: () => null } : {} }); context.exports = context.module.exports; vm.runInContext(code, context); const { normalizeAuthInfo, sessionMatchesAuth } = context.module.exports; const tenant = { id: "tenant-1", name: "Tenant", slug: "tenant" }; const base = { user: { id: "membership-1", account_id: "account-1", email: "person@example.test", password_reset_required: true }, tenant, principal: { auth_method: "session", account_id: "account-1", membership_id: "membership-1", session_id: "session-1", scopes: [] } }; test("normalization preserves an explicit required action and local password capability", () => { const normalized = normalizeAuthInfo({ ...base, user: { ...base.user, required_auth_action: "change_password", local_password: true } }); assert.equal(normalized.user.required_auth_action, "change_password"); assert.equal(normalized.user.local_password, true); assert.equal(normalized.scopes.length, 0); }); test("legacy password-reset metadata remains advisory without the server action", () => { const normalized = normalizeAuthInfo(base); assert.equal(normalized.user.password_reset_required, true); assert.equal(normalized.user.required_auth_action, null); assert.equal(normalized.user.local_password, false); }); test("lightweight session changes trigger full auth refresh for required actions and provider changes", () => { const auth = normalizeAuthInfo({ ...base, user: { ...base.user, required_auth_action: null, local_password: true } }); const session = { user: { ...auth.user }, tenant, active_tenant: tenant, auth_method: "session", session_id: "session-1" }; assert.equal(sessionMatchesAuth(session, auth), true); assert.equal(sessionMatchesAuth({ ...session, user: { ...session.user, required_auth_action: "change_password" } }, auth), false); assert.equal(sessionMatchesAuth({ ...session, user: { ...session.user, local_password: false } }, auth), false); assert.equal(sessionMatchesAuth({ ...session, session_id: "rotated-session" }, auth), false); }); const authorityContext = vm.createContext({ module: { exports: {} } }); authorityContext.exports = authorityContext.module.exports; vm.runInContext(transformSync(readFileSync(new URL("../src/api/authAuthority.ts", import.meta.url), "utf8"), { loader: "ts", format: "cjs" }).code, authorityContext); const { authAuthorityKey } = authorityContext.module.exports; const settings = { apiBaseUrl: "https://fixture.invalid", apiKey: "fixture-key", accessToken: "fixture-token" }; test("authority fences ignore fresh object identity and cosmetic profile changes", () => { const auth = normalizeAuthInfo(base); const key = authAuthorityKey(auth, settings); const refreshed = structuredClone(auth); refreshed.user.display_name = "Changed display name"; refreshed.user.preferred_language = "de"; refreshed.user.ui_preferences = { compact_tables: true }; refreshed.tenant.name = "Changed tenant name"; refreshed.profile_loaded = true; assert.equal(authAuthorityKey(refreshed, { ...settings }), key); }); test("authority fences include principal, tenant, credential, scope, acting and required-action changes", () => { const auth = normalizeAuthInfo(base); const key = authAuthorityKey(auth, settings); for (const mutate of [ (value) => { value.user.account_id = "different-account"; }, (value) => { value.user.email = "different@example.test"; }, (value) => { value.user.is_tenant_admin = true; }, (value) => { value.user.required_auth_action = "change_password"; }, (value) => { value.user.local_password = !value.user.local_password; }, (value) => { value.tenant.id = "different-tenant"; }, (value) => { value.active_tenant = { ...value.tenant, id: "active-tenant" }; }, (value) => { value.tenant.is_active = false; }, (value) => { value.scopes = ["new:permission"]; }, (value) => { value.roles = [{ id: "role", slug: "role", permissions: ["new:permission"] }]; }, (value) => { value.groups = [{ id: "group" }]; }, (value) => { value.principal.session_id = "rotated-session"; }, (value) => { value.principal.acting_assignment_id = "assignment"; }, (value) => { value.principal.acting_for_account_id = "actor"; }, (value) => { value.principal.delegation_ids = ["delegation"]; } ]) { const changed = structuredClone(auth); mutate(changed); assert.notEqual(authAuthorityKey(changed, settings), key); } for (const field of ["apiBaseUrl", "apiKey", "accessToken"]) { assert.notEqual(authAuthorityKey(auth, { ...settings, [field]: "changed" }), key); } }); test("authority set ordering and duplicate entries do not invent a context change", () => { const auth = normalizeAuthInfo(base); auth.scopes = ["b", "a", "b"]; auth.principal.group_ids = ["g2", "g1"]; const reordered = structuredClone(auth); reordered.scopes = ["a", "b"]; reordered.principal.group_ids = ["g1", "g2", "g1"]; assert.equal(authAuthorityKey(auth, settings), authAuthorityKey(reordered, settings)); });