import { Prisma } from "@capakraken/db"; import { resolvePermissions, SystemRole } from "@capakraken/shared"; import { describe, expect, it, vi } from "vitest"; import { userRouter } from "../router/user.js"; import { createCallerFactory } from "../trpc.js"; const { totpValidateMock } = vi.hoisted(() => ({ totpValidateMock: vi.fn(), })); const { argon2HashMock } = vi.hoisted(() => ({ argon2HashMock: vi.fn(), })); vi.mock("../lib/audit.js", () => ({ createAuditEntry: vi.fn().mockResolvedValue(undefined), })); vi.mock("@node-rs/argon2", () => ({ hash: argon2HashMock, })); vi.mock("otpauth", () => { class Secret { base32: string; constructor() { this.base32 = "MOCKSECRET"; } static fromBase32(value: string) { return value; } } class TOTP { validate(args: { token: string; window: number }) { return totpValidateMock(args); } toString() { return "otpauth://mock"; } } return { Secret, TOTP }; }); const createCaller = createCallerFactory(userRouter); function createAdminCaller(db: Record) { return createCaller({ session: { user: { email: "admin@example.com", name: "Admin", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_admin", systemRole: SystemRole.ADMIN, permissionOverrides: null, }, roleDefaults: null, }); } describe("user.linkResource", () => { it("returns NOT_FOUND when the target user does not exist", async () => { const userFindUnique = vi.fn().mockResolvedValue(null); const resourceFindUnique = vi.fn(); const updateMany = vi.fn(); const update = vi.fn(); const caller = createAdminCaller({ user: { findUnique: userFindUnique, }, resource: { findUnique: resourceFindUnique, updateMany, update, }, }); await expect(caller.linkResource({ userId: "missing_user", resourceId: "resource_1", })).rejects.toMatchObject({ code: "NOT_FOUND", message: "User not found", }); expect(userFindUnique).toHaveBeenCalledWith({ where: { id: "missing_user" }, select: { id: true }, }); expect(resourceFindUnique).not.toHaveBeenCalled(); expect(updateMany).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); }); it("returns NOT_FOUND when the target resource does not exist", async () => { const userFindUnique = vi.fn().mockResolvedValue({ id: "user_1" }); const resourceFindUnique = vi.fn().mockResolvedValue(null); const updateMany = vi.fn(); const caller = createAdminCaller({ user: { findUnique: userFindUnique, }, resource: { findUnique: resourceFindUnique, updateMany, }, }); await expect(caller.linkResource({ userId: "user_1", resourceId: "missing_resource", })).rejects.toMatchObject({ code: "NOT_FOUND", message: "Resource not found", }); expect(resourceFindUnique).toHaveBeenCalledWith({ where: { id: "missing_resource" }, select: { id: true, userId: true }, }); expect(updateMany).not.toHaveBeenCalled(); }); it("returns CONFLICT when the requested resource is already linked to another user", async () => { const userFindUnique = vi.fn().mockResolvedValue({ id: "user_1" }); const resourceFindUnique = vi.fn().mockResolvedValue({ id: "resource_1", userId: "user_2" }); const updateMany = vi.fn(); const caller = createAdminCaller({ user: { findUnique: userFindUnique, }, resource: { findUnique: resourceFindUnique, updateMany, }, }); await expect(caller.linkResource({ userId: "user_1", resourceId: "resource_1", })).rejects.toMatchObject({ code: "CONFLICT", message: "Resource is already linked to another user", }); expect(updateMany).not.toHaveBeenCalled(); }); it("unlinks existing assignments before linking the requested resource", async () => { const userFindUnique = vi.fn().mockResolvedValue({ id: "user_1" }); const resourceFindUnique = vi.fn().mockResolvedValue({ id: "resource_1", userId: null }); const updateMany = vi.fn() .mockResolvedValueOnce({ count: 1 }) .mockResolvedValueOnce({ count: 1 }); const caller = createAdminCaller({ user: { findUnique: userFindUnique, }, resource: { findUnique: resourceFindUnique, updateMany, }, }); const result = await caller.linkResource({ userId: "user_1", resourceId: "resource_1", }); expect(result).toEqual({ success: true }); expect(updateMany).toHaveBeenNthCalledWith(1, { where: { userId: "user_1", NOT: { id: "resource_1" }, }, data: { userId: null }, }); expect(updateMany).toHaveBeenNthCalledWith(2, { where: { id: "resource_1", OR: [{ userId: null }, { userId: "user_1" }], }, data: { userId: "user_1" }, }); }); it("unlinks a user without checking or updating a replacement resource", async () => { const userFindUnique = vi.fn().mockResolvedValue({ id: "user_1" }); const resourceFindUnique = vi.fn(); const updateMany = vi.fn().mockResolvedValue({ count: 1 }); const caller = createAdminCaller({ user: { findUnique: userFindUnique, }, resource: { findUnique: resourceFindUnique, updateMany, }, }); const result = await caller.linkResource({ userId: "user_1", resourceId: null, }); expect(result).toEqual({ success: true }); expect(userFindUnique).toHaveBeenCalledWith({ where: { id: "user_1" }, select: { id: true }, }); expect(resourceFindUnique).not.toHaveBeenCalled(); expect(updateMany).toHaveBeenCalledWith({ where: { userId: "user_1" }, data: { userId: null }, }); }); it("returns CONFLICT when the resource link changes between validation and update", async () => { const userFindUnique = vi.fn().mockResolvedValue({ id: "user_1" }); const resourceFindUnique = vi.fn().mockResolvedValue({ id: "resource_1", userId: null }); const updateMany = vi.fn() .mockResolvedValueOnce({ count: 0 }) .mockResolvedValueOnce({ count: 0 }); const caller = createAdminCaller({ user: { findUnique: userFindUnique, }, resource: { findUnique: resourceFindUnique, updateMany, }, }); await expect(caller.linkResource({ userId: "user_1", resourceId: "resource_1", })).rejects.toMatchObject({ code: "CONFLICT", message: "Resource link changed during update. Please retry.", }); }); }); describe("user admin account management", () => { it("counts users active within the last five minutes", async () => { const nowSpy = vi.spyOn(Date, "now").mockReturnValue(new Date("2026-03-30T20:00:00.000Z").valueOf()); const count = vi.fn().mockResolvedValue(4); const caller = createAdminCaller({ user: { count, }, }); const result = await caller.activeCount(); expect(result).toEqual({ count: 4 }); expect(count).toHaveBeenCalledWith({ where: { lastActiveAt: { gte: new Date("2026-03-30T19:55:00.000Z") }, }, }); nowSpy.mockRestore(); }); it("hashes and stores a replacement password for an existing user", async () => { argon2HashMock.mockReset(); argon2HashMock.mockResolvedValue("hashed-secret"); const findUnique = vi.fn().mockResolvedValue({ id: "user_2", name: "Alice", email: "alice@example.com", }); const update = vi.fn().mockResolvedValue({}); const caller = createAdminCaller({ user: { findUnique, update, }, }); const result = await caller.setPassword({ userId: "user_2", password: "secret123", }); expect(result).toEqual({ success: true }); expect(argon2HashMock).toHaveBeenCalledWith("secret123"); expect(update).toHaveBeenCalledWith({ where: { id: "user_2" }, data: { passwordHash: "hashed-secret" }, }); }); it("updates the selected user's display name", async () => { const findUnique = vi.fn().mockResolvedValue({ id: "user_2", name: "Alice", email: "alice@example.com", }); const update = vi.fn().mockResolvedValue({ id: "user_2", name: "Alice Updated", email: "alice@example.com", }); const caller = createAdminCaller({ user: { findUnique, update, }, }); const result = await caller.updateName({ id: "user_2", name: "Alice Updated", }); expect(result).toEqual({ id: "user_2", name: "Alice Updated", email: "alice@example.com", }); expect(findUnique).toHaveBeenCalledWith({ where: { id: "user_2" }, select: { id: true, name: true, email: true }, }); expect(update).toHaveBeenCalledWith({ where: { id: "user_2" }, data: { name: "Alice Updated" }, select: { id: true, name: true, email: true }, }); }); }); describe("user.autoLinkAllByEmail", () => { it("links only users with an unclaimed resource that matches their email", async () => { const userFindMany = vi.fn().mockResolvedValue([ { id: "user_1", email: "alice@example.com" }, { id: "user_2", email: "bob@example.com" }, { id: "user_3", email: "carol@example.com" }, ]); const resourceFindFirst = vi .fn() .mockResolvedValueOnce({ id: "resource_1" }) .mockResolvedValueOnce(null) .mockResolvedValueOnce({ id: "resource_3" }); const resourceUpdate = vi.fn().mockResolvedValue({}); const caller = createAdminCaller({ user: { findMany: userFindMany, }, resource: { findFirst: resourceFindFirst, update: resourceUpdate, }, }); const result = await caller.autoLinkAllByEmail(); expect(result).toEqual({ linked: 2, checked: 3 }); expect(userFindMany).toHaveBeenCalledWith({ where: { resource: null }, select: { id: true, email: true }, }); expect(resourceFindFirst).toHaveBeenNthCalledWith(1, { where: { email: "alice@example.com", userId: null }, select: { id: true }, }); expect(resourceFindFirst).toHaveBeenNthCalledWith(2, { where: { email: "bob@example.com", userId: null }, select: { id: true }, }); expect(resourceFindFirst).toHaveBeenNthCalledWith(3, { where: { email: "carol@example.com", userId: null }, select: { id: true }, }); expect(resourceUpdate).toHaveBeenCalledTimes(2); expect(resourceUpdate).toHaveBeenNthCalledWith(1, { where: { id: "resource_1" }, data: { userId: "user_1" }, }); expect(resourceUpdate).toHaveBeenNthCalledWith(2, { where: { id: "resource_3" }, data: { userId: "user_3" }, }); }); }); describe("user permission overrides", () => { it("stores explicit permission overrides for a user", async () => { const before = { id: "user_2", name: "Alice", email: "alice@example.com", permissionOverrides: null, }; const updated = { id: "user_2", permissionOverrides: { granted: ["manageProjects"], denied: ["viewCosts"], chapterIds: ["chapter_design"], }, }; const findUnique = vi.fn().mockResolvedValue(before); const update = vi.fn().mockResolvedValue(updated); const caller = createAdminCaller({ user: { findUnique, update, }, }); const overrides = { granted: ["manageProjects"], denied: ["viewCosts"], chapterIds: ["chapter_design"], }; const result = await caller.setPermissions({ userId: "user_2", overrides, }); expect(result).toEqual(updated); expect(findUnique).toHaveBeenCalledWith({ where: { id: "user_2" }, select: { id: true, name: true, email: true, permissionOverrides: true }, }); expect(update).toHaveBeenCalledWith({ where: { id: "user_2" }, data: { permissionOverrides: overrides }, }); }); it("resets permission overrides back to role defaults", async () => { const before = { id: "user_2", name: "Alice", email: "alice@example.com", permissionOverrides: { granted: ["manageProjects"], denied: ["viewCosts"], }, }; const updated = { id: "user_2", permissionOverrides: null, }; const findUnique = vi.fn().mockResolvedValue(before); const update = vi.fn().mockResolvedValue(updated); const caller = createAdminCaller({ user: { findUnique, update, }, }); const result = await caller.resetPermissions({ userId: "user_2" }); expect(result).toEqual(updated); expect(findUnique).toHaveBeenCalledWith({ where: { id: "user_2" }, select: { id: true, name: true, email: true, permissionOverrides: true }, }); expect(update).toHaveBeenCalledWith({ where: { id: "user_2" }, data: { permissionOverrides: Prisma.DbNull }, }); }); it("returns resolved effective permissions together with stored overrides", async () => { const overrides = { granted: ["manageProjects"], denied: ["viewCosts"], chapterIds: ["chapter_design"], }; const findUnique = vi.fn().mockResolvedValue({ systemRole: SystemRole.MANAGER, permissionOverrides: overrides, }); const caller = createAdminCaller({ user: { findUnique, }, }); const result = await caller.getEffectivePermissions({ userId: "user_2" }); expect(findUnique).toHaveBeenCalledWith({ where: { id: "user_2" }, select: { systemRole: true, permissionOverrides: true }, }); expect(result).toEqual({ systemRole: SystemRole.MANAGER, effectivePermissions: Array.from(resolvePermissions(SystemRole.MANAGER, overrides)), overrides, }); }); }); describe("user profile and TOTP self-service", () => { it("returns the authenticated user profile by db user id", async () => { const findUnique = vi.fn().mockResolvedValue({ id: "user_admin", name: "Admin", email: "admin@example.com", systemRole: SystemRole.ADMIN, permissionOverrides: null, createdAt: new Date("2026-03-30T08:00:00.000Z"), }); const caller = createAdminCaller({ user: { findUnique, }, }); const result = await caller.me(); expect(findUnique).toHaveBeenCalledWith({ where: { id: "user_admin" }, select: { id: true, name: true, email: true, systemRole: true, permissionOverrides: true, createdAt: true, }, }); expect(result).toEqual({ id: "user_admin", name: "Admin", email: "admin@example.com", systemRole: SystemRole.ADMIN, permissionOverrides: null, createdAt: new Date("2026-03-30T08:00:00.000Z"), }); }); it("uses the db user id for self-service profile reads even when the session email is stale", async () => { const findUnique = vi.fn().mockResolvedValue({ id: "user_admin", name: "Admin", email: "admin@example.com", systemRole: SystemRole.ADMIN, permissionOverrides: null, createdAt: new Date("2026-03-30T08:00:00.000Z"), }); const caller = createCaller({ session: { user: { email: "stale@example.com", name: "Admin", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: { user: { findUnique, }, } as never, dbUser: { id: "user_admin", systemRole: SystemRole.ADMIN, permissionOverrides: null, }, roleDefaults: null, }); await caller.me(); expect(findUnique).toHaveBeenCalledWith({ where: { id: "user_admin" }, select: { id: true, name: true, email: true, systemRole: true, permissionOverrides: true, createdAt: true, }, }); }); it("stores a new TOTP secret for the current user and returns the enrollment URI", async () => { const update = vi.fn().mockResolvedValue({}); const caller = createAdminCaller({ user: { update, }, }); const result = await caller.generateTotpSecret(); expect(update).toHaveBeenCalledWith({ where: { id: "user_admin" }, data: { totpSecret: "MOCKSECRET" }, }); expect(result).toEqual({ secret: "MOCKSECRET", uri: "otpauth://mock", }); }); it("rejects enabling TOTP when no secret was generated", async () => { const findUnique = vi.fn().mockResolvedValue({ id: "user_admin", name: "Admin", email: "admin@example.com", totpSecret: null, totpEnabled: false, }); const update = vi.fn(); const caller = createAdminCaller({ user: { findUnique, update, }, }); await expect(caller.verifyAndEnableTotp({ token: "123456" })).rejects.toMatchObject({ code: "BAD_REQUEST", message: "No TOTP secret generated. Call generateTotpSecret first.", }); expect(update).not.toHaveBeenCalled(); }); it("rejects enabling TOTP when it is already active", async () => { const findUnique = vi.fn().mockResolvedValue({ id: "user_admin", name: "Admin", email: "admin@example.com", totpSecret: "MOCKSECRET", totpEnabled: true, }); const update = vi.fn(); const caller = createAdminCaller({ user: { findUnique, update, }, }); await expect(caller.verifyAndEnableTotp({ token: "123456" })).rejects.toMatchObject({ code: "BAD_REQUEST", message: "TOTP is already enabled.", }); expect(update).not.toHaveBeenCalled(); }); it("rejects invalid TOTP tokens during self-service enablement", async () => { totpValidateMock.mockReset(); totpValidateMock.mockReturnValue(null); const findUnique = vi.fn().mockResolvedValue({ id: "user_admin", name: "Admin", email: "admin@example.com", totpSecret: "MOCKSECRET", totpEnabled: false, }); const update = vi.fn(); const caller = createAdminCaller({ user: { findUnique, update, }, }); await expect(caller.verifyAndEnableTotp({ token: "123456" })).rejects.toMatchObject({ code: "BAD_REQUEST", message: "Invalid TOTP token.", }); expect(totpValidateMock).toHaveBeenCalledWith({ token: "123456", window: 1, }); expect(update).not.toHaveBeenCalled(); }); it("enables TOTP for the current user when the token validates", async () => { totpValidateMock.mockReset(); totpValidateMock.mockReturnValue(0); const findUnique = vi.fn().mockResolvedValue({ id: "user_admin", name: "Admin", email: "admin@example.com", totpSecret: "MOCKSECRET", totpEnabled: false, }); const update = vi.fn().mockResolvedValue({}); const caller = createAdminCaller({ user: { findUnique, update, }, }); const result = await caller.verifyAndEnableTotp({ token: "123456" }); expect(result).toEqual({ enabled: true }); expect(update).toHaveBeenCalledWith({ where: { id: "user_admin" }, data: { totpEnabled: true }, }); }); it("verifies login-flow TOTP tokens successfully when enabled", async () => { totpValidateMock.mockReset(); totpValidateMock.mockReturnValue(0); const findUnique = vi.fn().mockResolvedValue({ id: "user_admin", totpSecret: "MOCKSECRET", totpEnabled: true, }); const caller = createAdminCaller({ user: { findUnique, }, }); const result = await caller.verifyTotp({ userId: "user_admin", token: "123456" }); expect(result).toEqual({ valid: true }); expect(findUnique).toHaveBeenCalledWith({ where: { id: "user_admin" }, select: { id: true, totpSecret: true, totpEnabled: true }, }); }); it("rejects invalid login-flow TOTP tokens with UNAUTHORIZED", async () => { totpValidateMock.mockReset(); totpValidateMock.mockReturnValue(null); const findUnique = vi.fn().mockResolvedValue({ id: "user_admin", totpSecret: "MOCKSECRET", totpEnabled: true, }); const caller = createAdminCaller({ user: { findUnique, }, }); await expect(caller.verifyTotp({ userId: "user_admin", token: "123456" })).rejects.toMatchObject({ code: "UNAUTHORIZED", message: "Invalid TOTP token.", }); }); }); describe("user dashboard and favorites", () => { it("falls back to the normalized default dashboard layout when stored data is invalid", async () => { const findUnique = vi.fn().mockResolvedValue({ dashboardLayout: { widgets: [ { id: "peakTimes", position: { x: 0, y: 0, w: 4, h: 3 } }, ], }, updatedAt: new Date("2026-03-30T18:00:00.000Z"), }); const caller = createAdminCaller({ user: { findUnique, }, }); const result = await caller.getDashboardLayout(); expect(findUnique).toHaveBeenCalledWith({ where: { id: "user_admin" }, select: { dashboardLayout: true, updatedAt: true }, }); expect(result).toEqual({ layout: { version: 2, gridCols: 12, widgets: [], }, updatedAt: new Date("2026-03-30T18:00:00.000Z"), }); }); it("persists a valid dashboard layout for the current user", async () => { const update = vi.fn().mockResolvedValue({ updatedAt: new Date("2026-03-30T19:00:00.000Z"), }); const caller = createAdminCaller({ user: { update, }, }); const layout = { version: 2, gridCols: 12, widgets: [], }; const result = await caller.saveDashboardLayout({ layout }); expect(result).toEqual({ updatedAt: new Date("2026-03-30T19:00:00.000Z"), }); expect(update).toHaveBeenCalledWith({ where: { id: "user_admin" }, data: { dashboardLayout: layout }, select: { updatedAt: true }, }); }); it("persists dashboard layout by db user id even when the session email is stale", async () => { const update = vi.fn().mockResolvedValue({ updatedAt: new Date("2026-03-30T19:30:00.000Z"), }); const caller = createCaller({ session: { user: { email: "stale@example.com", name: "Admin", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: { user: { update, }, } as never, dbUser: { id: "user_admin", systemRole: SystemRole.ADMIN, permissionOverrides: null, }, roleDefaults: null, }); await caller.saveDashboardLayout({ layout: { version: 2, gridCols: 12, widgets: [], }, }); expect(update).toHaveBeenCalledWith({ where: { id: "user_admin" }, data: { dashboardLayout: { version: 2, gridCols: 12, widgets: [], }, }, select: { updatedAt: true }, }); }); it("returns favorite project ids as an empty list when none are stored", async () => { const findUnique = vi.fn().mockResolvedValue({ favoriteProjectIds: null, }); const caller = createAdminCaller({ user: { findUnique, }, }); const result = await caller.getFavoriteProjectIds(); expect(result).toEqual([]); expect(findUnique).toHaveBeenCalledWith({ where: { id: "user_admin" }, select: { favoriteProjectIds: true }, }); }); it("adds a project to favorites when it is not already present", async () => { const findUnique = vi.fn().mockResolvedValue({ favoriteProjectIds: ["project_1"], }); const update = vi.fn().mockResolvedValue({}); const caller = createAdminCaller({ user: { findUnique, update, }, }); const result = await caller.toggleFavoriteProject({ projectId: "project_2" }); expect(result).toEqual({ favoriteProjectIds: ["project_1", "project_2"], added: true, }); expect(update).toHaveBeenCalledWith({ where: { id: "user_admin" }, data: { favoriteProjectIds: ["project_1", "project_2"] }, }); }); it("removes a project from favorites when it is already present", async () => { const findUnique = vi.fn().mockResolvedValue({ favoriteProjectIds: ["project_1", "project_2"], }); const update = vi.fn().mockResolvedValue({}); const caller = createAdminCaller({ user: { findUnique, update, }, }); const result = await caller.toggleFavoriteProject({ projectId: "project_2" }); expect(result).toEqual({ favoriteProjectIds: ["project_1"], added: false, }); expect(update).toHaveBeenCalledWith({ where: { id: "user_admin" }, data: { favoriteProjectIds: ["project_1"] }, }); }); }); describe("user column preferences and MFA status", () => { it("returns empty column preferences when nothing is stored", async () => { const findUnique = vi.fn().mockResolvedValue({ columnPreferences: null, }); const caller = createAdminCaller({ user: { findUnique, }, }); const result = await caller.getColumnPreferences(); expect(result).toEqual({}); expect(findUnique).toHaveBeenCalledWith({ where: { id: "user_admin" }, select: { columnPreferences: true }, }); }); it("merges column preferences without dropping untouched fields", async () => { const findUnique = vi.fn().mockResolvedValue({ columnPreferences: { resources: { visible: ["name", "role"], sort: { field: "name", dir: "asc" }, rowOrder: ["name", "role", "email"], }, }, }); const update = vi.fn().mockResolvedValue({ id: "user_admin" }); const caller = createAdminCaller({ user: { findUnique, update, }, }); const result = await caller.setColumnPreferences({ view: "resources", visible: ["name", "email"], }); expect(result).toEqual({ ok: true }); expect(update).toHaveBeenCalledWith({ where: { id: "user_admin" }, data: { columnPreferences: { resources: { visible: ["name", "email"], sort: { field: "name", dir: "asc" }, rowOrder: ["name", "role", "email"], }, }, }, }); }); it("reports the current MFA enabled state", async () => { const findUnique = vi.fn().mockResolvedValue({ totpEnabled: true, }); const caller = createAdminCaller({ user: { findUnique, }, }); const result = await caller.getMfaStatus(); expect(result).toEqual({ totpEnabled: true }); expect(findUnique).toHaveBeenCalledWith({ where: { id: "user_admin" }, select: { totpEnabled: true }, }); }); it("returns NOT_FOUND for MFA status when the current user record no longer exists", async () => { const findUnique = vi.fn().mockResolvedValue(null); const caller = createAdminCaller({ user: { findUnique, }, }); await expect(caller.getMfaStatus()).rejects.toMatchObject({ code: "NOT_FOUND", message: "User not found", }); }); });