import { SystemRole } from "@capakraken/shared"; import { describe, expect, it, vi } from "vitest"; import { roleRouter } from "../router/role.js"; import { createCallerFactory } from "../trpc.js"; const createCaller = createCallerFactory(roleRouter); 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, }; } describe("role router authorization", () => { describe("unauthenticated access", () => { it("rejects unauthenticated list call with UNAUTHORIZED", async () => { const roleFindMany = vi.fn(); const caller = createCaller( createContext({ role: { findMany: roleFindMany } }, { session: false }), ); await expect(caller.list({})).rejects.toMatchObject({ code: "UNAUTHORIZED", message: "Authentication required", }); expect(roleFindMany).not.toHaveBeenCalled(); }); it("rejects unauthenticated create call with UNAUTHORIZED", async () => { const roleCreate = vi.fn(); const caller = createCaller( createContext({ role: { create: roleCreate } }, { session: false }), ); await expect( caller.create({ name: "Art Director" }), ).rejects.toMatchObject({ code: "UNAUTHORIZED", message: "Authentication required", }); expect(roleCreate).not.toHaveBeenCalled(); }); }); describe("USER role — insufficient permissions for mutations", () => { it("forbids USER from calling create", async () => { const roleCreate = vi.fn(); const caller = createCaller( createContext({ role: { create: roleCreate } }), ); await expect( caller.create({ name: "Art Director" }), ).rejects.toMatchObject({ code: "FORBIDDEN", message: "Manager or Admin role required", }); expect(roleCreate).not.toHaveBeenCalled(); }); it("forbids USER from calling update", async () => { const roleUpdate = vi.fn(); const caller = createCaller( createContext({ role: { update: roleUpdate } }), ); await expect( caller.update({ id: "role_1", data: { name: "Updated Role" } }), ).rejects.toMatchObject({ code: "FORBIDDEN", message: "Manager or Admin role required", }); expect(roleUpdate).not.toHaveBeenCalled(); }); it("forbids USER from calling delete", async () => { const roleDelete = vi.fn(); const caller = createCaller( createContext({ role: { delete: roleDelete } }), ); await expect(caller.delete({ id: "role_1" })).rejects.toMatchObject({ code: "FORBIDDEN", message: "Manager or Admin role required", }); expect(roleDelete).not.toHaveBeenCalled(); }); it("forbids USER from calling deactivate", async () => { const roleUpdate = vi.fn(); const caller = createCaller( createContext({ role: { update: roleUpdate } }), ); await expect(caller.deactivate({ id: "role_1" })).rejects.toMatchObject({ code: "FORBIDDEN", message: "Manager or Admin role required", }); expect(roleUpdate).not.toHaveBeenCalled(); }); }); describe("MANAGER role — permitted for mutations", () => { it("allows MANAGER to call create without auth error", async () => { const createdRole = { id: "role_new", name: "Art Director", description: null, color: null, isActive: true, _count: { resourceRoles: 0 }, }; const roleCreate = vi.fn().mockResolvedValue(createdRole); const roleFindUnique = vi.fn().mockResolvedValue(null); // name not taken const auditLogCreate = vi.fn().mockResolvedValue({}); // planningEntry count queries (attachZeroAllocationCount path) const planningEntryFindMany = vi.fn().mockResolvedValue([]); const db: Record = { role: { create: roleCreate, findUnique: roleFindUnique }, auditLog: { create: auditLogCreate }, planningEntry: { findMany: planningEntryFindMany }, $transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)), }; const caller = createCaller( createContext(db, { role: SystemRole.MANAGER }), ); // Should not throw UNAUTHORIZED or FORBIDDEN const result = await caller.create({ name: "Art Director" }); expect(result).toMatchObject({ id: "role_new", name: "Art Director" }); expect(roleCreate).toHaveBeenCalledTimes(1); }); }); describe("ADMIN role — permitted for mutations", () => { it("allows ADMIN to call delete without auth error", async () => { const existingRole = { id: "role_1", name: "Stale Role", description: null, color: null, isActive: true, _count: { resourceRoles: 0 }, }; const roleFindUnique = vi.fn().mockResolvedValue(existingRole); const roleDelete = vi.fn().mockResolvedValue(existingRole); const auditLogCreate = vi.fn().mockResolvedValue({}); // attachSingleRolePlanningEntryCount calls countPlanningEntries // which needs both demandRequirement and assignment findMany const demandRequirementFindMany = vi.fn().mockResolvedValue([]); const assignmentFindMany = vi.fn().mockResolvedValue([]); const adminDb: Record = { role: { findUnique: roleFindUnique, delete: roleDelete, }, auditLog: { create: auditLogCreate }, demandRequirement: { findMany: demandRequirementFindMany }, assignment: { findMany: assignmentFindMany }, $transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(adminDb)), }; const caller = createCaller( createContext(adminDb, { role: SystemRole.ADMIN }), ); const result = await caller.delete({ id: "role_1" }); expect(result).toEqual({ success: true }); expect(roleDelete).toHaveBeenCalledTimes(1); }); }); });