import { SystemRole } from "@capakraken/shared"; import { describe, expect, it, vi } from "vitest"; import { userRouter } from "../router/user.js"; import { createCallerFactory } from "../trpc.js"; const createCaller = createCallerFactory(userRouter); function createContext( db: Record, options: { role?: SystemRole; session?: boolean; } = {}, ) { const { role = SystemRole.USER, session = true } = options; return { session: session ? { user: { email: "user@example.com", name: "User", image: null }, expires: "2099-01-01T00:00:00.000Z", } : null, db: db as never, dbUser: session ? { id: role === SystemRole.ADMIN ? "user_admin" : "user_1", systemRole: role, permissionOverrides: null, } : null, roleDefaults: null, }; } describe("user router authorization", () => { it("requires authentication for self-service profile lookups", async () => { const findUnique = vi.fn(); const caller = createCaller(createContext({ user: { findUnique, }, }, { session: false })); await expect(caller.me()).rejects.toMatchObject({ code: "UNAUTHORIZED", message: "Authentication required", }); expect(findUnique).not.toHaveBeenCalled(); }); it("requires authentication for dashboard layout reads", async () => { const findUnique = vi.fn(); const caller = createCaller(createContext({ user: { findUnique, }, }, { session: false })); await expect(caller.getDashboardLayout()).rejects.toMatchObject({ code: "UNAUTHORIZED", message: "Authentication required", }); expect(findUnique).not.toHaveBeenCalled(); }); it("requires authentication for favorite project toggles", async () => { const findUnique = vi.fn(); const update = vi.fn(); const caller = createCaller(createContext({ user: { findUnique, update, }, }, { session: false })); await expect(caller.toggleFavoriteProject({ projectId: "project_1" })).rejects.toMatchObject({ code: "UNAUTHORIZED", message: "Authentication required", }); expect(findUnique).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); }); it("requires authentication for dashboard layout saves", async () => { const update = vi.fn(); const caller = createCaller(createContext({ user: { update, }, }, { session: false })); await expect(caller.saveDashboardLayout({ layout: { version: 2, gridCols: 12, widgets: [] }, })).rejects.toMatchObject({ code: "UNAUTHORIZED", message: "Authentication required", }); expect(update).not.toHaveBeenCalled(); }); it("requires authentication for favorite project reads", async () => { const findUnique = vi.fn(); const caller = createCaller(createContext({ user: { findUnique, }, }, { session: false })); await expect(caller.getFavoriteProjectIds()).rejects.toMatchObject({ code: "UNAUTHORIZED", message: "Authentication required", }); expect(findUnique).not.toHaveBeenCalled(); }); it("requires authentication for column preference reads and writes", async () => { const findUnique = vi.fn(); const update = vi.fn(); const caller = createCaller(createContext({ user: { findUnique, update, }, }, { session: false })); await expect(caller.getColumnPreferences()).rejects.toMatchObject({ code: "UNAUTHORIZED", message: "Authentication required", }); await expect(caller.setColumnPreferences({ view: "resources", visible: ["name"], })).rejects.toMatchObject({ code: "UNAUTHORIZED", message: "Authentication required", }); expect(findUnique).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); }); it("requires authentication for MFA status lookups", async () => { const findUniqueOrThrow = vi.fn(); const caller = createCaller(createContext({ user: { findUniqueOrThrow, }, }, { session: false })); await expect(caller.getMfaStatus()).rejects.toMatchObject({ code: "UNAUTHORIZED", message: "Authentication required", }); expect(findUniqueOrThrow).not.toHaveBeenCalled(); }); it("forbids regular users from listing assignable users", async () => { const findMany = vi.fn(); const caller = createCaller(createContext({ user: { findMany, }, })); await expect(caller.listAssignable()).rejects.toMatchObject({ code: "FORBIDDEN", message: "Manager or Admin role required", }); expect(findMany).not.toHaveBeenCalled(); }); it("allows managers to list assignable users", async () => { const findMany = vi.fn().mockResolvedValue([ { id: "user_1", name: "Alice", email: "alice@example.com" }, ]); const caller = createCaller(createContext({ user: { findMany, }, }, { role: SystemRole.MANAGER })); const result = await caller.listAssignable(); expect(result).toHaveLength(1); expect(findMany).toHaveBeenCalledWith({ select: { id: true, name: true, email: true, }, orderBy: { name: "asc" }, }); }); it("forbids non-admin users from reading effective permissions", async () => { const findUnique = vi.fn(); const caller = createCaller(createContext({ user: { findUnique, }, }, { role: SystemRole.MANAGER })); await expect(caller.getEffectivePermissions({ userId: "user_2" })).rejects.toMatchObject({ code: "FORBIDDEN", message: "Admin role required", }); expect(findUnique).not.toHaveBeenCalled(); }); it("forbids non-admin users from setting explicit permission overrides", async () => { const findUnique = vi.fn(); const update = vi.fn(); const caller = createCaller(createContext({ user: { findUnique, update, }, }, { role: SystemRole.MANAGER })); await expect(caller.setPermissions({ userId: "user_2", overrides: { granted: ["manageProjects"], }, })).rejects.toMatchObject({ code: "FORBIDDEN", message: "Admin role required", }); expect(findUnique).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); }); it("forbids non-admin users from resetting permission overrides", async () => { const findUnique = vi.fn(); const update = vi.fn(); const caller = createCaller(createContext({ user: { findUnique, update, }, }, { role: SystemRole.MANAGER })); await expect(caller.resetPermissions({ userId: "user_2" })).rejects.toMatchObject({ code: "FORBIDDEN", message: "Admin role required", }); expect(findUnique).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); }); it("forbids non-admin users from disabling TOTP for other users", async () => { const findUnique = vi.fn(); const caller = createCaller(createContext({ user: { findUnique, }, }, { role: SystemRole.MANAGER })); await expect(caller.disableTotp({ userId: "user_2" })).rejects.toMatchObject({ code: "FORBIDDEN", message: "Admin role required", }); expect(findUnique).not.toHaveBeenCalled(); }); it("forbids non-admin users from linking resources", async () => { const userFindUnique = vi.fn(); const resourceFindUnique = vi.fn(); const updateMany = vi.fn(); const update = vi.fn(); const caller = createCaller(createContext({ user: { findUnique: userFindUnique, }, resource: { findUnique: resourceFindUnique, updateMany, update, }, }, { role: SystemRole.MANAGER })); await expect(caller.linkResource({ userId: "user_2", resourceId: "resource_1", })).rejects.toMatchObject({ code: "FORBIDDEN", message: "Admin role required", }); expect(userFindUnique).not.toHaveBeenCalled(); expect(resourceFindUnique).not.toHaveBeenCalled(); expect(updateMany).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); }); it("forbids non-admin users from auto-linking users by email", async () => { const userFindMany = vi.fn(); const resourceFindFirst = vi.fn(); const resourceUpdate = vi.fn(); const caller = createCaller(createContext({ user: { findMany: userFindMany, }, resource: { findFirst: resourceFindFirst, update: resourceUpdate, }, }, { role: SystemRole.MANAGER })); await expect(caller.autoLinkAllByEmail()).rejects.toMatchObject({ code: "FORBIDDEN", message: "Admin role required", }); expect(userFindMany).not.toHaveBeenCalled(); expect(resourceFindFirst).not.toHaveBeenCalled(); expect(resourceUpdate).not.toHaveBeenCalled(); }); it("keeps TOTP verification public for the login flow", async () => { const findUnique = vi.fn().mockResolvedValue({ id: "user_1", totpEnabled: false, totpSecret: null, }); const caller = createCaller(createContext({ user: { findUnique, }, }, { session: false })); await expect(caller.verifyTotp({ userId: "user_1", token: "123456" })).rejects.toMatchObject({ code: "BAD_REQUEST", message: "TOTP is not enabled for this user.", }); expect(findUnique).toHaveBeenCalledWith({ where: { id: "user_1" }, select: { id: true, totpSecret: true, totpEnabled: true }, }); }); });