import { SystemRole } from "@capakraken/shared"; import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("@capakraken/application", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, isChargeabilityActualBooking: actual.isChargeabilityActualBooking, isChargeabilityRelevantProject: actual.isChargeabilityRelevantProject, listAssignmentBookings: vi.fn(), recomputeResourceValueScores: vi.fn(), }; }); vi.mock("../router/blueprint-validation.js", () => ({ assertBlueprintDynamicFields: vi.fn().mockResolvedValue(undefined), })); vi.mock("../ai-client.js", () => ({ createAiClient: vi.fn(() => ({ chat: { completions: { create: vi.fn().mockResolvedValue({ choices: [{ message: { content: "Generated summary" } }], }), }, }, responses: { create: vi.fn(), }, })), isAiConfigured: vi.fn().mockReturnValue(true), loggedAiCall: vi.fn(async (_provider, _model, _promptLength, fn) => fn()), })); vi.mock("../lib/anonymization.js", () => ({ anonymizeResource: vi.fn((r: Record) => r), anonymizeResources: vi.fn((rs: unknown[]) => rs), anonymizeSearchMatches: vi.fn((rs: unknown[]) => rs), getAnonymizationDirectory: vi.fn().mockResolvedValue(null), resolveResourceIdsByDisplayedEids: vi.fn().mockResolvedValue(new Map()), })); vi.mock("../lib/logger.js", () => ({ logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }, })); import { resourceRouter } from "../router/resource.js"; import { createAiClient, isAiConfigured, loggedAiCall } from "../ai-client.js"; import { getAnonymizationDirectory } from "../lib/anonymization.js"; import { createCallerFactory } from "../trpc.js"; const createCaller = createCallerFactory(resourceRouter); function createManagerCaller(db: Record) { return createCaller({ session: { user: { email: "manager@example.com", name: "Manager", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "mgr_1", 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: "admin_1", systemRole: SystemRole.ADMIN, permissionOverrides: null, }, }); } function createProtectedCaller(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 sampleResource = { id: "res_1", eid: "E-001", displayName: "Alice", email: "alice@example.com", chapter: "CGI", lcrCents: 5000, ucrCents: 9000, currency: "EUR", chargeabilityTarget: 80, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, }, skills: [], dynamicFields: {}, blueprintId: null, blueprint: null, isActive: true, createdAt: new Date("2026-03-01"), updatedAt: new Date("2026-03-01"), roleId: null, portfolioUrl: null, postalCode: null, federalState: null, valueScore: null, valueScoreBreakdown: null, valueScoreUpdatedAt: null, userId: null, resourceRoles: [], areaRole: null, countryId: null, metroCityId: null, orgUnitId: null, managementLevelGroupId: null, managementLevelId: null, resourceType: null, chgResponsibility: null, rolledOff: false, departed: false, enterpriseId: null, clientUnitId: null, fte: 1, }; describe("resource router CRUD", () => { beforeEach(() => { vi.clearAllMocks(); }); // ─── listStaff ──────────────────────────────────────────────────────────── describe("listStaff", () => { it("returns paginated results with total count for staff callers", async () => { const db = { resource: { findMany: vi.fn().mockResolvedValue([sampleResource]), count: vi.fn().mockResolvedValue(1), }, }; const caller = createManagerCaller(db); const result = await caller.listStaff({ limit: 50 }); expect(result.resources).toHaveLength(1); expect(result.resources[0]?.displayName).toBe("Alice"); expect(db.resource.findMany).toHaveBeenCalled(); }); it("applies search filter for staff callers", async () => { const db = { resource: { findMany: vi.fn().mockResolvedValue([]), count: vi.fn().mockResolvedValue(0), }, }; const caller = createManagerCaller(db); await caller.listStaff({ search: "Alice", limit: 50 }); expect(db.resource.findMany).toHaveBeenCalled(); }); }); // ─── getById ────────────────────────────────────────────────────────────── describe("getById", () => { it("returns correct resource", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_1" }), findUnique: vi.fn().mockResolvedValue({ ...sampleResource, userId: "user_1" }), findMany: vi.fn().mockResolvedValue([]), }, systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, }; const caller = createProtectedCaller(db); const result = await caller.getById({ id: "res_1" }); expect(result.id).toBe("res_1"); expect(result.displayName).toBe("Alice"); }); it("throws NOT_FOUND when resource does not exist", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_1" }), findUnique: vi.fn().mockResolvedValue(null), findMany: vi.fn().mockResolvedValue([]), }, systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, }; const caller = createProtectedCaller(db); await expect(caller.getById({ id: "missing" })).rejects.toThrow( expect.objectContaining({ code: "NOT_FOUND" }), ); }); it("sets isOwnedByCurrentUser when userId matches", async () => { const ownedResource = { ...sampleResource, userId: "user_1" }; const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_1" }), findUnique: vi.fn().mockResolvedValue(ownedResource), findMany: vi.fn().mockResolvedValue([]), }, systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, }; const caller = createProtectedCaller(db); const result = await caller.getById({ id: "res_1" }); expect(result.isOwnedByCurrentUser).toBe(true); }); it("rejects foreign resources for regular users", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_own" }), findUnique: vi.fn().mockResolvedValue(sampleResource), findMany: vi.fn().mockResolvedValue([]), }, }; const caller = createProtectedCaller(db); await expect(caller.getById({ id: "res_1" })).rejects.toThrow( expect.objectContaining({ code: "FORBIDDEN" }), ); }); }); describe("getByEid", () => { it("returns the matching resource for staff callers", async () => { const db = { resource: { findUnique: vi.fn().mockResolvedValue(sampleResource), }, }; const caller = createManagerCaller(db); const result = await caller.getByEid({ eid: "E-001" }); expect(result).toMatchObject({ id: "res_1", displayName: "Alice", eid: "E-001", }); expect(db.resource.findUnique).toHaveBeenCalledWith({ where: { eid: "E-001" } }); }); it("throws NOT_FOUND when the requested eid does not exist", async () => { const db = { resource: { findUnique: vi.fn().mockResolvedValue(null), }, }; const caller = createManagerCaller(db); await expect(caller.getByEid({ eid: "E-404" })).rejects.toThrow( expect.objectContaining({ code: "NOT_FOUND" }), ); }); it("resolves anonymized alias eids through the directory fallback", async () => { vi.mocked(getAnonymizationDirectory).mockResolvedValueOnce({ byAliasEid: new Map([["alias-e-001", "res_1"]]), } as Awaited>); const findUnique = vi .fn() .mockResolvedValueOnce(null) .mockResolvedValueOnce(sampleResource); const db = { resource: { findUnique, }, }; const caller = createManagerCaller(db); const result = await caller.getByEid({ eid: "alias-e-001" }); expect(result).toMatchObject({ id: "res_1", eid: "E-001", }); expect(findUnique).toHaveBeenNthCalledWith(1, { where: { eid: "alias-e-001" } }); expect(findUnique).toHaveBeenNthCalledWith(2, { where: { id: "res_1" } }); }); }); // ─── create ─────────────────────────────────────────────────────────────── describe("create", () => { it("creates a resource and returns it", async () => { const created = { ...sampleResource, id: "res_new", resourceRoles: [] }; const db = { resource: { findFirst: vi.fn().mockResolvedValue(null), create: vi.fn().mockResolvedValue(created), }, auditLog: { create: vi.fn().mockResolvedValue({}) }, }; const caller = createManagerCaller(db); const result = await caller.create({ eid: "E-NEW", displayName: "New Resource", email: "new@example.com", lcrCents: 4000, ucrCents: 8000, }); expect(result.id).toBe("res_new"); expect(db.resource.create).toHaveBeenCalled(); expect(db.auditLog.create).toHaveBeenCalled(); }); it("throws CONFLICT on duplicate eid or email", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue(sampleResource), create: vi.fn(), }, auditLog: { create: vi.fn() }, }; const caller = createManagerCaller(db); await expect( caller.create({ eid: "E-001", displayName: "Duplicate", email: "alice@example.com", lcrCents: 5000, ucrCents: 9000, }), ).rejects.toThrow( expect.objectContaining({ code: "CONFLICT" }), ); }); it("blocks USER role from creating resources", async () => { const db = { resource: { findFirst: vi.fn(), create: vi.fn() }, auditLog: { create: vi.fn() }, }; const caller = createProtectedCaller(db); await expect( caller.create({ eid: "E-002", displayName: "Blocked", email: "blocked@example.com", lcrCents: 4000, ucrCents: 8000, }), ).rejects.toThrow( expect.objectContaining({ code: "FORBIDDEN" }), ); }); }); // ─── update ─────────────────────────────────────────────────────────────── describe("update", () => { it("updates resource fields", async () => { const updated = { ...sampleResource, displayName: "Alice Updated" }; const db = { resource: { findUnique: vi.fn().mockResolvedValue(sampleResource), update: vi.fn().mockResolvedValue(updated), }, resourceRole: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) }, auditLog: { create: vi.fn().mockResolvedValue({}) }, }; const caller = createManagerCaller(db); const result = await caller.update({ id: "res_1", data: { displayName: "Alice Updated" }, }); expect(result.displayName).toBe("Alice Updated"); expect(db.resource.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "res_1" } }), ); }); it("throws NOT_FOUND when resource does not exist", async () => { const db = { resource: { findUnique: vi.fn().mockResolvedValue(null), update: vi.fn(), }, auditLog: { create: vi.fn() }, }; const caller = createManagerCaller(db); await expect( caller.update({ id: "missing", data: { displayName: "X" } }), ).rejects.toThrow( expect.objectContaining({ code: "NOT_FOUND" }), ); }); }); // ─── deactivate ─────────────────────────────────────────────────────────── describe("deactivate", () => { it("sets isActive to false", async () => { const deactivated = { ...sampleResource, isActive: false }; const db = { resource: { update: vi.fn().mockResolvedValue(deactivated), }, auditLog: { create: vi.fn().mockResolvedValue({}) }, }; const caller = createManagerCaller(db); const result = await caller.deactivate({ id: "res_1" }); expect(result.isActive).toBe(false); expect(db.resource.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "res_1" }, data: { isActive: false }, }), ); }); }); describe("batchDeactivate", () => { it("deactivates all requested resources and records one audit entry", async () => { const update = vi .fn() .mockResolvedValueOnce({ ...sampleResource, id: "res_1", isActive: false }) .mockResolvedValueOnce({ ...sampleResource, id: "res_2", isActive: false }); const db = { resource: { update, }, $transaction: vi.fn(async (operations: Promise[]) => Promise.all(operations)), auditLog: { create: vi.fn().mockResolvedValue({}) }, }; const caller = createManagerCaller(db); const result = await caller.batchDeactivate({ ids: ["res_1", "res_2"] }); expect(result).toEqual({ count: 2 }); expect(db.$transaction).toHaveBeenCalledTimes(1); expect(db.auditLog.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ entityType: "Resource", action: "UPDATE", }), }), ); }); }); describe("chapters", () => { it("returns active chapter names in ascending order", async () => { const db = { resource: { findMany: vi.fn().mockResolvedValue([ { chapter: "Animation" }, { chapter: "CGI" }, ]), }, }; const caller = createProtectedCaller(db); const result = await caller.chapters(); expect(result).toEqual(["Animation", "CGI"]); expect(db.resource.findMany).toHaveBeenCalledWith({ where: { isActive: true, chapter: { not: null } }, select: { chapter: true }, distinct: ["chapter"], orderBy: { chapter: "asc" }, }); }); }); // ─── getHoverCard ───────────────────────────────────────────────────────── describe("getHoverCard", () => { it("returns expected shape with key fields", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_1" }), findUnique: vi.fn().mockResolvedValue({ id: "res_1", displayName: "Alice", eid: "E-001", email: "alice@example.com", chapter: "CGI", lcrCents: 5000, ucrCents: 9000, currency: "EUR", chargeabilityTarget: 80, skills: [], availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, isActive: true, areaRole: null, country: null, managementLevel: null, resourceType: null, }), }, systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, }; const caller = createProtectedCaller(db); const result = await caller.getHoverCard({ id: "res_1" }); expect(result).toMatchObject({ id: "res_1", displayName: "Alice", chapter: "CGI", lcrCents: 5000, isActive: true, }); }); it("throws NOT_FOUND for missing resource", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_1" }), findUnique: vi.fn().mockResolvedValue(null), }, systemSettings: { findUnique: vi.fn().mockResolvedValue(null) }, }; const caller = createProtectedCaller(db); await expect(caller.getHoverCard({ id: "missing" })).rejects.toThrow( expect.objectContaining({ code: "NOT_FOUND" }), ); }); it("rejects foreign hover-card access for regular users", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_own" }), findUnique: vi.fn().mockResolvedValue({ id: "res_1", displayName: "Alice", eid: "E-001", email: "alice@example.com", chapter: "CGI", lcrCents: 5000, ucrCents: 9000, currency: "EUR", chargeabilityTarget: 80, skills: [], availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, isActive: true, areaRole: null, country: null, managementLevel: null, resourceType: null, }), }, }; const caller = createProtectedCaller(db); await expect(caller.getHoverCard({ id: "res_1" })).rejects.toThrow( expect.objectContaining({ code: "FORBIDDEN" }), ); }); }); // ─── importSkillMatrix ──────────────────────────────────────────────────── describe("importSkillMatrix", () => { it("imports skills for the current user resource", async () => { const updatedResource = { ...sampleResource, skills: [{ skill: "Maya", proficiency: 4 }], skillMatrixUpdatedAt: new Date(), }; const db = { user: { findUnique: vi.fn().mockResolvedValue({ id: "user_1", email: "user@example.com", resource: { id: "res_1" }, }), }, resource: { update: vi.fn().mockResolvedValue(updatedResource), }, }; const caller = createProtectedCaller(db); const result = await caller.importSkillMatrix({ skills: [{ skill: "Maya", proficiency: 4 }], }); expect(db.user.findUnique).toHaveBeenCalledWith({ where: { id: "user_1" }, include: { resource: true }, }); expect(db.resource.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "res_1" }, }), ); }); it("throws NOT_FOUND when user has no linked resource", async () => { const db = { user: { findUnique: vi.fn().mockResolvedValue({ id: "user_1", email: "user@example.com", resource: null, }), }, }; const caller = createProtectedCaller(db); await expect( caller.importSkillMatrix({ skills: [{ skill: "Nuke", proficiency: 3 }], }), ).rejects.toThrow("No resource linked to your account"); expect(db.user.findUnique).toHaveBeenCalledWith({ where: { id: "user_1" }, include: { resource: true }, }); }); }); describe("importSkillMatrixForResource", () => { it("updates skills for an existing resource", async () => { const db = { resource: { findUnique: vi.fn().mockResolvedValue(sampleResource), update: vi.fn().mockResolvedValue(sampleResource), }, }; const caller = createManagerCaller(db); const result = await caller.importSkillMatrixForResource({ resourceId: "res_1", skills: [{ skill: "Houdini", proficiency: 5 }], }); expect(result).toEqual({ count: 1 }); expect(db.resource.findUnique).toHaveBeenCalledWith({ where: { id: "res_1" } }); expect(db.resource.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "res_1" }, }), ); }); it("throws NOT_FOUND when the target resource does not exist", async () => { const db = { resource: { findUnique: vi.fn().mockResolvedValue(null), update: vi.fn(), }, }; const caller = createManagerCaller(db); await expect( caller.importSkillMatrixForResource({ resourceId: "missing", skills: [{ skill: "Houdini", proficiency: 5 }], }), ).rejects.toThrow( expect.objectContaining({ code: "NOT_FOUND" }), ); expect(db.resource.update).not.toHaveBeenCalled(); }); }); describe("batchImportSkillMatrices", () => { it("updates all matching resources in one transaction and reports missing eids", async () => { const update = vi .fn() .mockResolvedValueOnce({ ...sampleResource, id: "res_1" }) .mockResolvedValueOnce({ ...sampleResource, id: "res_2", eid: "E-002" }); const db = { resource: { findMany: vi.fn().mockResolvedValue([ { id: "res_1", eid: "E-001" }, { id: "res_2", eid: "E-002" }, ]), update, }, $transaction: vi.fn(async (operations: Promise[]) => Promise.all(operations)), }; const caller = createAdminCaller(db); const result = await caller.batchImportSkillMatrices({ entries: [ { eid: "E-001", skills: [{ skill: "Houdini", proficiency: 5 }] }, { eid: "E-002", skills: [{ skill: "Nuke", proficiency: 4 }] }, { eid: "E-404", skills: [{ skill: "Maya", proficiency: 3 }] }, ], }); expect(result).toEqual({ updated: 2, notFound: 1 }); expect(db.resource.findMany).toHaveBeenCalledWith({ where: { eid: { in: ["E-001", "E-002", "E-404"] } }, select: { id: true, eid: true }, }); expect(db.$transaction).toHaveBeenCalledTimes(1); expect(db.resource.update).toHaveBeenCalledTimes(2); }); }); describe("generateAiSummary", () => { it("persists the generated summary for the requested resource", async () => { const update = vi.fn().mockResolvedValue({ ...sampleResource, aiSummary: "Generated summary", }); const db = { resource: { findUnique: vi.fn().mockResolvedValue({ ...sampleResource, skills: [ { skill: "Maya", proficiency: 5, isMainSkill: true }, { skill: "Nuke", proficiency: 4 }, ], areaRole: { name: "3D Artist" }, }), update, }, systemSettings: { findUnique: vi.fn().mockResolvedValue({ id: "singleton", aiProvider: "openai", azureOpenAiDeployment: "gpt-test", aiSummaryPrompt: null, aiMaxCompletionTokens: 200, aiTemperature: 0.7, }), }, }; const caller = createManagerCaller(db); const result = await caller.generateAiSummary({ resourceId: "res_1" }); expect(isAiConfigured).toHaveBeenCalledWith(expect.objectContaining({ azureOpenAiDeployment: "gpt-test", })); expect(createAiClient).toHaveBeenCalledWith(expect.objectContaining({ azureOpenAiDeployment: "gpt-test", })); expect(loggedAiCall).toHaveBeenCalledOnce(); expect(update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "res_1" }, data: expect.objectContaining({ aiSummary: "Generated summary", aiSummaryUpdatedAt: expect.any(Date), }), }), ); expect(result).toEqual({ summary: "Generated summary" }); }); it("fails fast when AI is not configured", async () => { vi.mocked(isAiConfigured).mockReturnValueOnce(false); const db = { resource: { findUnique: vi.fn().mockResolvedValue({ ...sampleResource, areaRole: { name: "3D Artist" }, }), }, systemSettings: { findUnique: vi.fn().mockResolvedValue({ id: "singleton", aiProvider: "openai", azureOpenAiDeployment: null, }), }, }; const caller = createManagerCaller(db); await expect(caller.generateAiSummary({ resourceId: "res_1" })).rejects.toThrow( "AI is not configured. Please set credentials in Admin → Settings.", ); expect(createAiClient).not.toHaveBeenCalled(); }); }); });