import { PermissionKey, SystemRole } from "@capakraken/shared"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { resourceRouter } from "../router/resource.js"; import { createCallerFactory } from "../trpc.js"; vi.mock("../lib/logger.js", () => ({ logger: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn(), }, })); const createCaller = createCallerFactory(resourceRouter); beforeEach(() => { vi.clearAllMocks(); }); function createManagerCaller(db: Record) { return createCaller({ session: { user: { email: "mgr@example.com", name: "Manager", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_mgr", systemRole: SystemRole.MANAGER, permissionOverrides: null, }, }); } 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, }, }); } function createUserCaller(db: Record) { return createCaller({ session: { user: { email: "user@example.com", name: "User", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_1", systemRole: SystemRole.USER, permissionOverrides: null, }, }); } const VALID_CREATE_INPUT = { eid: "EMP-001", displayName: "Jane Doe", email: "jane@example.com", chapter: "Engineering", lcrCents: 5000, ucrCents: 8000, currency: "EUR", chargeabilityTarget: 80, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, }, skills: [], dynamicFields: {}, }; const MOCK_CREATED_RESOURCE = { id: "res_new", ...VALID_CREATE_INPUT, resourceRoles: [], }; function mockDb(overrides: Record = {}) { return { resource: { findFirst: vi.fn().mockResolvedValue(null), findUnique: vi.fn().mockResolvedValue(null), findMany: vi.fn().mockResolvedValue([]), update: vi.fn().mockResolvedValue({ id: "res_1", isActive: false }), delete: vi.fn().mockResolvedValue({}), deleteMany: vi.fn().mockResolvedValue({ count: 0 }), }, blueprint: { findUnique: vi.fn().mockResolvedValue(null), findMany: vi.fn().mockResolvedValue([]), }, auditLog: { create: vi.fn().mockResolvedValue({}), createMany: vi.fn().mockResolvedValue({ count: 0 }), }, assignment: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), }, vacation: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), }, resourceRole: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 0 }), }, $transaction: vi.fn(async (fn: (tx: unknown) => unknown) => { const tx = { resource: { create: vi.fn().mockResolvedValue(MOCK_CREATED_RESOURCE), update: vi .fn() .mockResolvedValue({ id: "res_1", ...VALID_CREATE_INPUT, resourceRoles: [] }), delete: vi.fn().mockResolvedValue({}), deleteMany: vi.fn().mockResolvedValue({ count: 0 }), }, auditLog: { create: vi.fn().mockResolvedValue({}), createMany: vi.fn().mockResolvedValue({ count: 0 }), }, resourceRole: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 0 }), }, assignment: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), }, vacation: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), }, $executeRaw: vi.fn().mockResolvedValue(1), }; return fn(tx); }), $executeRaw: vi.fn().mockResolvedValue(1), ...overrides, }; } describe("resource create", () => { it("rejects non-manager users", async () => { const caller = createUserCaller(mockDb()); await expect(caller.create(VALID_CREATE_INPUT)).rejects.toMatchObject({ code: "FORBIDDEN", }); }); it("rejects duplicate EID or email", async () => { const db = mockDb({ resource: { findFirst: vi.fn().mockResolvedValue({ id: "existing", eid: "EMP-001" }), }, }); const caller = createManagerCaller(db); await expect(caller.create(VALID_CREATE_INPUT)).rejects.toMatchObject({ code: "CONFLICT", }); }); it("rejects more than one primary role", async () => { const caller = createManagerCaller(mockDb()); await expect( caller.create({ ...VALID_CREATE_INPUT, roles: [ { roleId: "role_1", isPrimary: true }, { roleId: "role_2", isPrimary: true }, ], }), ).rejects.toMatchObject({ code: "BAD_REQUEST", message: expect.stringContaining("primary role"), }); }); it("creates resource with audit log for managers", async () => { const db = mockDb(); const caller = createManagerCaller(db); const result = await caller.create(VALID_CREATE_INPUT); expect(result).toMatchObject({ id: "res_new" }); expect(db.$transaction).toHaveBeenCalled(); }); }); describe("resource update", () => { it("rejects non-manager users", async () => { const caller = createUserCaller(mockDb()); await expect( caller.update({ id: "res_1", data: { displayName: "Updated" } }), ).rejects.toMatchObject({ code: "FORBIDDEN" }); }); it("throws NOT_FOUND for non-existent resource", async () => { const db = mockDb({ resource: { ...mockDb().resource, findUnique: vi.fn().mockResolvedValue(null), }, }); const caller = createManagerCaller(db); await expect( caller.update({ id: "res_missing", data: { displayName: "Updated" } }), ).rejects.toMatchObject({ code: "NOT_FOUND" }); }); it("rejects multiple primary roles on update", async () => { const db = mockDb({ resource: { ...mockDb().resource, findUnique: vi.fn().mockResolvedValue({ id: "res_1", ...VALID_CREATE_INPUT, blueprintId: null, dynamicFields: {}, }), }, }); const caller = createManagerCaller(db); await expect( caller.update({ id: "res_1", data: { roles: [ { roleId: "role_1", isPrimary: true }, { roleId: "role_2", isPrimary: true }, ], }, }), ).rejects.toMatchObject({ code: "BAD_REQUEST", message: expect.stringContaining("primary role"), }); }); }); describe("resource deactivate", () => { it("rejects non-manager users", async () => { const caller = createUserCaller(mockDb()); await expect(caller.deactivate({ id: "res_1" })).rejects.toMatchObject({ code: "FORBIDDEN", }); }); it("soft-deletes resource for managers", async () => { const db = mockDb(); const caller = createManagerCaller(db); const result = await caller.deactivate({ id: "res_1" }); expect(result).toBeDefined(); expect(db.$transaction).toHaveBeenCalled(); }); }); describe("resource batchUpdateCustomFields", () => { it("rejects non-manager users", async () => { const caller = createUserCaller(mockDb()); await expect( caller.batchUpdateCustomFields({ ids: ["res_1"], fields: { department: "Engineering" }, }), ).rejects.toMatchObject({ code: "FORBIDDEN" }); }); it("validates field types (rejects invalid values)", async () => { const caller = createManagerCaller(mockDb()); // The hardened schema only accepts string | number | boolean | null await expect( caller.batchUpdateCustomFields({ ids: ["res_1"], // @ts-expect-error — intentionally passing an array to test schema validation fields: { department: ["nested", "array"] }, }), ).rejects.toThrow(); }); it("executes batch update with audit log", async () => { const db = mockDb(); const caller = createManagerCaller(db); const result = await caller.batchUpdateCustomFields({ ids: ["res_1", "res_2"], fields: { department: "Engineering", level: 3 }, }); expect(result).toEqual({ updated: 2 }); expect(db.$transaction).toHaveBeenCalled(); }); }); describe("resource hardDelete", () => { it("rejects non-admin users", async () => { const caller = createManagerCaller(mockDb()); await expect(caller.hardDelete({ id: "res_1" })).rejects.toMatchObject({ code: "FORBIDDEN", }); }); it("throws NOT_FOUND for missing resource", async () => { const db = mockDb({ resource: { ...mockDb().resource, findUnique: vi.fn().mockResolvedValue(null), }, }); const caller = createAdminCaller(db); await expect(caller.hardDelete({ id: "res_missing" })).rejects.toMatchObject({ code: "NOT_FOUND", }); }); it("deletes resource and cascades for admin", async () => { const db = mockDb({ resource: { ...mockDb().resource, findUnique: vi.fn().mockResolvedValue({ id: "res_1", displayName: "Jane", eid: "EMP-001" }), }, }); const caller = createAdminCaller(db); const result = await caller.hardDelete({ id: "res_1" }); expect(result).toEqual({ deleted: true }); expect(db.$transaction).toHaveBeenCalled(); }); });