import { SystemRole } from "@capakraken/shared"; import { describe, expect, it, vi } from "vitest"; const { createAuditEntry } = vi.hoisted(() => ({ createAuditEntry: vi.fn(), })); vi.mock("../lib/audit.js", () => ({ createAuditEntry, })); import { clientRouter } from "../router/client.js"; import { createCallerFactory } from "../trpc.js"; const createCaller = createCallerFactory(clientRouter); function createPlanningCaller(db: Record) { return createCaller({ session: { user: { email: "planning@example.com", name: "Planning", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_planning", systemRole: SystemRole.MANAGER, permissionOverrides: null, }, permissions: new Set(["view:planning"]), }); } 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: "user_manager", 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, }, }); } describe("client router", () => { it("lists clients with filters and count includes", async () => { const findMany = vi.fn().mockResolvedValue([{ id: "client_1", name: "Acme" }]); const caller = createPlanningCaller({ client: { findMany }, }); const result = await caller.list({ parentId: "parent_1", isActive: true, search: "Acme" }); expect(findMany).toHaveBeenCalledWith({ where: { parentId: "parent_1", isActive: true, OR: [ { name: { contains: "Acme", mode: "insensitive" } }, { code: { contains: "Acme", mode: "insensitive" } }, ], }, include: { _count: { select: { children: true, projects: true } } }, orderBy: [{ sortOrder: "asc" }, { name: "asc" }], }); expect(result).toHaveLength(1); }); it("returns a nested tree from ordered flat records", async () => { const findMany = vi.fn().mockResolvedValue([ { id: "client_root", name: "Root", code: "ROOT", parentId: null, isActive: true, sortOrder: 10, tags: [], createdAt: new Date("2026-03-01T00:00:00.000Z"), updatedAt: new Date("2026-03-01T00:00:00.000Z"), }, { id: "client_child", name: "Child", code: "CHILD", parentId: "client_root", isActive: true, sortOrder: 20, tags: [], createdAt: new Date("2026-03-01T00:00:00.000Z"), updatedAt: new Date("2026-03-01T00:00:00.000Z"), }, ]); const caller = createPlanningCaller({ client: { findMany }, }); const result = await caller.getTree({ isActive: true }); expect(findMany).toHaveBeenCalledWith({ where: { isActive: true }, orderBy: [{ sortOrder: "asc" }, { name: "asc" }], }); expect(result).toEqual([ expect.objectContaining({ id: "client_root", children: [expect.objectContaining({ id: "client_child" })], }), ]); }); it("resolves a client by identifier via the protected query", async () => { const findUnique = vi .fn() .mockResolvedValueOnce(null) .mockResolvedValueOnce({ id: "client_1", code: "ACME", name: "Acme", parentId: null, isActive: true, }); const caller = createPlanningCaller({ client: { findUnique, findFirst: vi.fn(), }, }); const result = await caller.resolveByIdentifier({ identifier: " ACME " }); expect(findUnique).toHaveBeenNthCalledWith(2, { where: { code: "ACME" }, select: { id: true, name: true, code: true, parentId: true, isActive: true, }, }); expect(result).toEqual({ id: "client_1", code: "ACME", name: "Acme", parentId: null, isActive: true, }); }); it("creates and updates a client through the router", async () => { const findUnique = vi .fn() .mockResolvedValueOnce({ id: "parent_1", name: "Parent" }) .mockResolvedValueOnce(null) .mockResolvedValueOnce({ id: "client_1", name: "Acme", code: "ACME", isActive: true, }); const create = vi.fn().mockResolvedValue({ id: "client_1", name: "Acme", code: "ACME", parentId: "parent_1", sortOrder: 10, tags: ["enterprise"], isActive: true, }); const update = vi.fn().mockResolvedValue({ id: "client_1", name: "Acme Updated", code: "ACME", isActive: true, }); const caller = createManagerCaller({ client: { findUnique, create, update }, }); const created = await caller.create({ name: "Acme", code: "ACME", parentId: "parent_1", sortOrder: 10, tags: ["enterprise"], }); const updated = await caller.update({ id: "client_1", data: { name: "Acme Updated", code: "ACME" }, }); expect(create).toHaveBeenCalledWith({ data: { name: "Acme", code: "ACME", parentId: "parent_1", sortOrder: 10, tags: ["enterprise"], }, }); expect(update).toHaveBeenCalledWith({ where: { id: "client_1" }, data: { name: "Acme Updated", code: "ACME" }, }); expect(created.id).toBe("client_1"); expect(updated.name).toBe("Acme Updated"); expect(createAuditEntry).toHaveBeenCalledTimes(2); }); it("deletes a deletable client through the admin router", async () => { const remove = vi.fn().mockResolvedValue({ id: "client_1" }); const caller = createAdminCaller({ client: { findUnique: vi.fn().mockResolvedValue({ id: "client_1", name: "Acme", _count: { projects: 0, children: 0 }, }), delete: remove, }, }); const result = await caller.delete({ id: "client_1" }); expect(remove).toHaveBeenCalledWith({ where: { id: "client_1" }, }); expect(result).toEqual({ id: "client_1", name: "Acme", _count: { projects: 0, children: 0 }, }); }); it("batch-updates sort order through the manager router", async () => { const update = vi.fn( ({ where, data }: { where: { id: string }; data: { sortOrder: number } }) => Promise.resolve({ id: where.id, sortOrder: data.sortOrder }), ); const $transaction = vi.fn().mockResolvedValue([]); const caller = createManagerCaller({ $transaction, client: { update }, }); const result = await caller.batchUpdateSortOrder([ { id: "client_1", sortOrder: 10 }, { id: "client_2", sortOrder: 20 }, ]); expect($transaction).toHaveBeenCalledTimes(1); expect(update).toHaveBeenNthCalledWith(1, { where: { id: "client_1" }, data: { sortOrder: 10 }, }); expect(update).toHaveBeenNthCalledWith(2, { where: { id: "client_2" }, data: { sortOrder: 20 }, }); expect(result).toEqual({ ok: true }); }); });