import { SystemRole } from "@capakraken/shared"; import { VacationStatus, VacationType } from "@capakraken/db"; import { describe, expect, it, vi, beforeEach } from "vitest"; import { createNotification } from "../lib/create-notification.js"; import { logger } from "../lib/logger.js"; import { dispatchWebhooks } from "../lib/webhook-dispatcher.js"; import { vacationRouter } from "../router/vacation.js"; import { createCallerFactory } from "../trpc.js"; vi.mock("../sse/event-bus.js", () => ({ emitVacationCreated: vi.fn(), emitVacationUpdated: vi.fn(), emitVacationDeleted: vi.fn(), emitNotificationCreated: vi.fn(), emitTaskAssigned: vi.fn(), })); vi.mock("../lib/email.js", () => ({ sendEmail: vi.fn(), })); vi.mock("../lib/create-notification.js", () => ({ createNotification: vi.fn().mockResolvedValue("notif_1"), })); vi.mock("../lib/vacation-conflicts.js", () => ({ checkVacationConflicts: vi.fn().mockResolvedValue({ warnings: [] }), checkBatchVacationConflicts: vi.fn().mockResolvedValue(new Map()), })); vi.mock("../lib/webhook-dispatcher.js", () => ({ dispatchWebhooks: vi.fn().mockResolvedValue(undefined), })); vi.mock("../lib/audit.js", () => ({ createAuditEntry: vi.fn().mockResolvedValue(undefined), })); vi.mock("../lib/logger.js", () => ({ logger: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn(), }, })); const createCaller = createCallerFactory(vacationRouter); function createProtectedCaller(db: Record) { return createCaller({ session: { user: { email: "user@example.com", name: "User", image: null }, expires: "2026-12-31T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_1", systemRole: SystemRole.USER, permissionOverrides: null, }, }); } function createManagerCaller(db: Record) { return createCaller({ session: { user: { email: "manager@example.com", name: "Manager", image: null }, expires: "2026-12-31T00: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: "2026-12-31T00:00:00.000Z", }, db: db as never, dbUser: { id: "admin_1", systemRole: SystemRole.ADMIN, permissionOverrides: null, }, }); } function createUnauthenticatedCaller(db: Record) { return createCaller({ session: null, db: db as never, dbUser: null, }); } const sampleVacation = { id: "vac_1", resourceId: "res_1", type: VacationType.ANNUAL, status: VacationStatus.PENDING, startDate: new Date("2026-06-01"), endDate: new Date("2026-06-05"), note: "Summer vacation", isHalfDay: false, halfDayPart: null, requestedById: "user_1", approvedById: null, approvedAt: null, rejectionReason: null, createdAt: new Date("2026-03-01"), updatedAt: new Date("2026-03-01"), resource: { id: "res_1", displayName: "Alice", eid: "E-001" }, requestedBy: { id: "user_1", name: "User", email: "user@example.com" }, approvedBy: null, }; function createVacationDb(overrides: Record = {}) { const db = { user: { findUnique: vi.fn().mockResolvedValue({ id: "user_1", systemRole: "USER" }), findMany: vi.fn().mockResolvedValue([{ id: "mgr_1" }, { id: "admin_1" }]), }, resource: { findUnique: vi.fn().mockImplementation(async (args?: { select?: Record }) => { const select = args?.select ?? {}; return { ...(select.userId ? { userId: "user_1" } : {}), ...(select.displayName ? { displayName: "Alice" } : {}), ...(select.user ? { user: null } : {}), ...(select.federalState ? { federalState: "BY" } : {}), ...(select.country ? { country: { code: "DE", name: "Germany" } } : {}), ...(select.metroCity ? { metroCity: null } : {}), }; }), count: vi.fn().mockResolvedValue(0), }, vacation: { findFirst: vi.fn().mockResolvedValue(null), findUnique: vi.fn().mockResolvedValue(sampleVacation), findMany: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue(sampleVacation), update: vi.fn().mockResolvedValue(sampleVacation), updateMany: vi.fn().mockResolvedValue({ count: 1 }), }, notification: { updateMany: vi.fn().mockResolvedValue({ count: 1 }), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, }; return { ...db, ...overrides, user: { ...db.user, ...(overrides.user as Record | undefined) }, resource: { ...db.resource, ...(overrides.resource as Record | undefined) }, vacation: { ...db.vacation, ...(overrides.vacation as Record | undefined) }, notification: { ...db.notification, ...(overrides.notification as Record | undefined), }, auditLog: { ...db.auditLog, ...(overrides.auditLog as Record | undefined) }, }; } describe("vacation router", () => { describe("list", () => { it("returns vacations with default filters", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_1" }), }, vacation: { findMany: vi.fn().mockResolvedValue([sampleVacation]), }, }; const caller = createProtectedCaller(db); const result = await caller.list({}); expect(result).toHaveLength(1); expect(result[0].id).toBe("vac_1"); expect(db.vacation.findMany).toHaveBeenCalledWith( expect.objectContaining({ orderBy: { startDate: "asc" }, take: 100, }), ); }); it("applies resourceId filter", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_1" }), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }; const caller = createProtectedCaller(db); await caller.list({ resourceId: "res_1" }); expect(db.vacation.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ resourceId: "res_1" }), }), ); }); it("scopes regular users to their own resource when no filter is provided", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_own" }), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }; const caller = createProtectedCaller(db); await caller.list({}); expect(db.vacation.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ resourceId: "res_own" }), }), ); }); it("forbids regular users from listing another resource's vacations", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_own" }), }, vacation: { findMany: vi.fn(), }, }; const caller = createProtectedCaller(db); await expect(caller.list({ resourceId: "res_other" })).rejects.toThrow( "You can only view vacation data for your own resource", ); expect(db.vacation.findMany).not.toHaveBeenCalled(); }); it("applies status and type filters", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_1" }), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }; const caller = createProtectedCaller(db); await caller.list({ status: VacationStatus.APPROVED, type: VacationType.SICK, }); expect(db.vacation.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ status: VacationStatus.APPROVED, type: VacationType.SICK, }), }), ); }); it("rejects unauthenticated users", async () => { const db = { vacation: { findMany: vi.fn() } }; const caller = createUnauthenticatedCaller(db); await expect(caller.list({})).rejects.toThrow("Authentication required"); }); }); describe("background side effects", () => { it("logs and swallows async notification failures during approval", async () => { vi.mocked(createNotification).mockRejectedValueOnce(new Error("notification down")); const db = createVacationDb({ user: { findUnique: vi.fn().mockResolvedValue({ id: "mgr_1", systemRole: "MANAGER" }), }, resource: { findUnique: vi.fn().mockImplementation(async (args?: { select?: Record }) => { const select = args?.select ?? {}; return { ...(select.displayName ? { displayName: "Alice" } : {}), ...(select.user ? { user: { id: "user_1", email: "user@example.com", name: "User" } } : {}), }; }), }, vacation: { findUnique: vi.fn().mockResolvedValue({ ...sampleVacation, status: VacationStatus.PENDING, }), update: vi.fn().mockResolvedValue({ ...sampleVacation, status: VacationStatus.APPROVED, }), }, }); const caller = createManagerCaller(db); const result = await caller.approve({ id: "vac_1" }); await Promise.resolve(); await Promise.resolve(); expect(result.status).toBe(VacationStatus.APPROVED); expect(vi.mocked(logger.error)).toHaveBeenCalledWith( expect.objectContaining({ effectName: "notifyVacationStatus", vacationId: "vac_1", resourceId: "res_1", newStatus: VacationStatus.APPROVED, }), "Vacation background side effect failed", ); }); it("logs and swallows webhook failures during approval", async () => { vi.mocked(dispatchWebhooks).mockRejectedValueOnce(new Error("webhook down")); const db = createVacationDb({ user: { findUnique: vi.fn().mockResolvedValue({ id: "mgr_1", systemRole: "MANAGER" }), }, resource: { findUnique: vi.fn().mockImplementation(async (args?: { select?: Record }) => { const select = args?.select ?? {}; return { ...(select.displayName ? { displayName: "Alice" } : {}), ...(select.user ? { user: { id: "user_1", email: "user@example.com", name: "User" } } : {}), }; }), }, vacation: { findUnique: vi.fn().mockResolvedValue({ ...sampleVacation, status: VacationStatus.PENDING, }), update: vi.fn().mockResolvedValue({ ...sampleVacation, status: VacationStatus.APPROVED, }), }, }); const caller = createManagerCaller(db); const result = await caller.approve({ id: "vac_1" }); await Promise.resolve(); await Promise.resolve(); expect(result.status).toBe(VacationStatus.APPROVED); expect(vi.mocked(logger.error)).toHaveBeenCalledWith( expect.objectContaining({ effectName: "dispatchWebhooks", event: "vacation.approved" }), "Vacation background side effect failed", ); }); }); describe("getById", () => { it("returns vacation by id", async () => { const db = { resource: { findFirst: vi.fn(), }, vacation: { findUnique: vi.fn().mockResolvedValue({ ...sampleVacation, resource: { ...sampleVacation.resource, userId: "user_1" }, }), }, }; const caller = createProtectedCaller(db); const result = await caller.getById({ id: "vac_1" }); expect(result.id).toBe("vac_1"); expect(db.vacation.findUnique).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "vac_1" }, }), ); }); it("forbids regular users from reading another user's vacation", async () => { const db = { vacation: { findUnique: vi.fn().mockResolvedValue({ ...sampleVacation, requestedById: "someone_else", resource: { ...sampleVacation.resource, userId: "someone_else" }, }), }, }; const caller = createProtectedCaller(db); await expect(caller.getById({ id: "vac_1" })).rejects.toThrow( "You can only view your own vacation data", ); }); it("throws NOT_FOUND for missing vacation", async () => { const db = { vacation: { findUnique: vi.fn().mockResolvedValue(null), }, }; const caller = createProtectedCaller(db); await expect(caller.getById({ id: "missing" })).rejects.toThrow("Vacation not found"); }); }); describe("create", () => { it("creates vacation as PENDING for regular user", async () => { const createdVacation = { ...sampleVacation, status: VacationStatus.PENDING, }; const db = createVacationDb({ vacation: { create: vi.fn().mockResolvedValue(createdVacation), }, }); const caller = createProtectedCaller(db); const result = await caller.create({ resourceId: "res_1", type: VacationType.ANNUAL, startDate: new Date("2026-06-01"), endDate: new Date("2026-06-05"), }); expect(result.status).toBe(VacationStatus.PENDING); expect(db.vacation.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ status: VacationStatus.PENDING, resourceId: "res_1", type: VacationType.ANNUAL, }), }), ); }); it("creates vacation as APPROVED for manager", async () => { const createdVacation = { ...sampleVacation, status: VacationStatus.APPROVED, approvedById: "mgr_1", }; const db = createVacationDb({ user: { findUnique: vi.fn().mockResolvedValue({ id: "mgr_1", systemRole: "MANAGER" }), }, vacation: { create: vi.fn().mockResolvedValue(createdVacation), }, }); const caller = createManagerCaller(db); const result = await caller.create({ resourceId: "res_1", type: VacationType.ANNUAL, startDate: new Date("2026-06-01"), endDate: new Date("2026-06-05"), }); expect(result.status).toBe(VacationStatus.APPROVED); expect(db.vacation.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ status: VacationStatus.APPROVED, approvedById: "mgr_1", }), }), ); }); it("rejects overlapping vacation", async () => { const db = createVacationDb({ vacation: { findFirst: vi.fn().mockResolvedValue({ id: "existing_vac" }), }, }); const caller = createProtectedCaller(db); await expect( caller.create({ resourceId: "res_1", type: VacationType.ANNUAL, startDate: new Date("2026-06-01"), endDate: new Date("2026-06-05"), }), ).rejects.toThrow("Overlapping vacation already exists"); }); it("rejects when end date is before start date", async () => { const db = createVacationDb({ user: { findUnique: vi.fn() }, vacation: { findFirst: vi.fn() }, }); const caller = createProtectedCaller(db); await expect( caller.create({ resourceId: "res_1", type: VacationType.ANNUAL, startDate: new Date("2026-06-05"), endDate: new Date("2026-06-01"), }), ).rejects.toThrow(); }); it("supports half-day vacations", async () => { const createdVacation = { ...sampleVacation, isHalfDay: true, halfDayPart: "MORNING", }; const db = createVacationDb({ vacation: { create: vi.fn().mockResolvedValue(createdVacation), }, }); const caller = createProtectedCaller(db); const result = await caller.create({ resourceId: "res_1", type: VacationType.ANNUAL, startDate: new Date("2026-06-01"), endDate: new Date("2026-06-01"), isHalfDay: true, halfDayPart: "MORNING", }); expect(result.isHalfDay).toBe(true); expect(db.vacation.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ isHalfDay: true, halfDayPart: "MORNING", }), }), ); }); it("rejects multi-day half-day vacations", async () => { const db = createVacationDb(); const caller = createProtectedCaller(db); await expect(caller.create({ resourceId: "res_1", type: VacationType.ANNUAL, startDate: new Date("2026-06-01"), endDate: new Date("2026-06-02"), isHalfDay: true, halfDayPart: "MORNING", })).rejects.toThrow(); expect(db.vacation.create).not.toHaveBeenCalled(); }); it("rejects half-day vacations without a half-day part", async () => { const db = createVacationDb(); const caller = createProtectedCaller(db); await expect(caller.create({ resourceId: "res_1", type: VacationType.ANNUAL, startDate: new Date("2026-06-01"), endDate: new Date("2026-06-01"), isHalfDay: true, })).rejects.toThrow(); expect(db.vacation.create).not.toHaveBeenCalled(); }); it("rejects half-day parts on full-day vacations", async () => { const db = createVacationDb(); const caller = createProtectedCaller(db); await expect(caller.create({ resourceId: "res_1", type: VacationType.ANNUAL, startDate: new Date("2026-06-01"), endDate: new Date("2026-06-01"), halfDayPart: "AFTERNOON", })).rejects.toThrow(); expect(db.vacation.create).not.toHaveBeenCalled(); }); it("rejects leave requests that only hit public holidays", async () => { const db = createVacationDb({ vacation: { findMany: vi.fn().mockResolvedValue([]), }, }); const caller = createProtectedCaller(db); await expect(caller.create({ resourceId: "res_1", type: VacationType.ANNUAL, startDate: new Date("2026-01-06T00:00:00.000Z"), endDate: new Date("2026-01-06T00:00:00.000Z"), })).rejects.toThrow("does not deduct any vacation days"); expect(db.vacation.create).not.toHaveBeenCalled(); }); }); describe("previewRequest", () => { it("shows public holidays as non-deductible leave days", async () => { const db = createVacationDb({ resource: { findUnique: vi.fn().mockResolvedValue({ userId: "user_1", federalState: "BY", country: { code: "DE", name: "Germany" }, metroCity: { name: "Augsburg" }, }), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }); const caller = createProtectedCaller(db); const result = await caller.previewRequest({ resourceId: "res_1", type: VacationType.ANNUAL, startDate: new Date("2028-08-08T00:00:00.000Z"), endDate: new Date("2028-08-08T00:00:00.000Z"), }); expect(result.requestedDays).toBe(1); expect(result.effectiveDays).toBe(0); expect(result.deductedDays).toBe(0); expect(result.publicHolidayDates).toContain("2028-08-08"); expect(result.holidayContext).toEqual({ countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: "Augsburg", sources: { hasCalendarHolidays: true, hasLegacyPublicHolidayEntries: false, }, }); expect(result.holidayDetails).toContainEqual({ date: "2028-08-08", source: "CALENDAR", }); }); it("uses custom city holiday calendars for non-deductible leave days", async () => { const db = createVacationDb({ resource: { findUnique: vi.fn().mockResolvedValue({ userId: "user_1", countryId: "country_de", metroCityId: "city_muc", federalState: "BY", country: { code: "DE", name: "Germany" }, metroCity: { name: "Muenchen" }, }), }, holidayCalendar: { findMany: vi.fn().mockResolvedValue([ { id: "cal_muc", name: "Muenchen lokal", scopeType: "CITY", priority: 10, createdAt: new Date("2026-01-01T00:00:00.000Z"), entries: [ { date: new Date("2020-11-15T00:00:00.000Z"), name: "Lokaler Stadtfeiertag", isRecurringAnnual: true, }, ], }, ]), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }); const caller = createProtectedCaller(db); const result = await caller.previewRequest({ resourceId: "res_1", type: VacationType.ANNUAL, startDate: new Date("2026-11-15T00:00:00.000Z"), endDate: new Date("2026-11-15T00:00:00.000Z"), }); expect(result.requestedDays).toBe(1); expect(result.effectiveDays).toBe(0); expect(result.publicHolidayDates).toContain("2026-11-15"); expect(result.holidayContext.countryName).toBe("Germany"); expect(result.holidayContext.metroCityName).toBe("Muenchen"); expect(db.holidayCalendar.findMany).toHaveBeenCalled(); }); it("marks legacy public holiday entries as a separate preview source", async () => { const db = createVacationDb({ resource: { findUnique: vi.fn().mockResolvedValue({ userId: "user_1", federalState: "HH", country: { code: "DE", name: "Germany" }, metroCity: { name: "Hamburg" }, }), }, vacation: { findMany: vi.fn().mockResolvedValue([ { startDate: new Date("2026-05-01T00:00:00.000Z"), endDate: new Date("2026-05-01T00:00:00.000Z"), }, ]), }, }); const caller = createProtectedCaller(db); const result = await caller.previewRequest({ resourceId: "res_1", type: VacationType.ANNUAL, startDate: new Date("2026-05-01T00:00:00.000Z"), endDate: new Date("2026-05-01T00:00:00.000Z"), }); expect(result.publicHolidayDates).toContain("2026-05-01"); expect(result.holidayContext.sources).toEqual({ hasCalendarHolidays: true, hasLegacyPublicHolidayEntries: true, }); expect(result.holidayDetails).toContainEqual({ date: "2026-05-01", source: "CALENDAR_AND_LEGACY", }); }); it("rejects multi-day half-day previews", async () => { const db = createVacationDb(); const caller = createProtectedCaller(db); await expect(caller.previewRequest({ resourceId: "res_1", type: VacationType.ANNUAL, startDate: new Date("2026-06-01"), endDate: new Date("2026-06-02"), isHalfDay: true, })).rejects.toThrow(); }); }); describe("create manual public holiday handling", () => { it("rejects manual public holiday creation requests", async () => { const db = createVacationDb(); const caller = createManagerCaller(db); await expect(caller.create({ resourceId: "res_1", type: VacationType.PUBLIC_HOLIDAY, startDate: new Date("2026-05-01T00:00:00.000Z"), endDate: new Date("2026-05-01T00:00:00.000Z"), })).rejects.toThrow("Public holidays must be managed via Holiday Calendars or the legacy holiday import"); expect(db.vacation.create).not.toHaveBeenCalled(); }); }); describe("approve", () => { it("approves a PENDING vacation", async () => { const updatedVacation = { ...sampleVacation, status: VacationStatus.APPROVED, approvedById: "mgr_1", }; const db = createVacationDb({ vacation: { findUnique: vi.fn().mockResolvedValue(sampleVacation), update: vi.fn().mockResolvedValue(updatedVacation), }, user: { findUnique: vi.fn().mockResolvedValue({ id: "mgr_1" }), }, resource: { findUnique: vi.fn().mockResolvedValue(null), }, }); const caller = createManagerCaller(db); const result = await caller.approve({ id: "vac_1" }); expect(result.status).toBe(VacationStatus.APPROVED); expect(db.vacation.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "vac_1" }, data: expect.objectContaining({ status: VacationStatus.APPROVED, rejectionReason: null, }), }), ); }); it("throws NOT_FOUND for missing vacation", async () => { const db = createVacationDb({ vacation: { findUnique: vi.fn().mockResolvedValue(null), }, }); const caller = createManagerCaller(db); await expect(caller.approve({ id: "missing" })).rejects.toThrow("Vacation not found"); }); it("rejects approving an already APPROVED vacation", async () => { const db = createVacationDb({ vacation: { findUnique: vi.fn().mockResolvedValue({ ...sampleVacation, status: VacationStatus.APPROVED, }), }, }); const caller = createManagerCaller(db); await expect(caller.approve({ id: "vac_1" })).rejects.toThrow( "Only PENDING, CANCELLED, or REJECTED vacations can be approved", ); }); it("forbids regular users from approving", async () => { const db = {}; const caller = createProtectedCaller(db); await expect(caller.approve({ id: "vac_1" })).rejects.toThrow(); }); }); describe("reject", () => { it("rejects a PENDING vacation", async () => { const updatedVacation = { ...sampleVacation, status: VacationStatus.REJECTED, rejectionReason: "Team conflict", }; const db = createVacationDb({ vacation: { findUnique: vi.fn().mockResolvedValue(sampleVacation), update: vi.fn().mockResolvedValue(updatedVacation), }, resource: { findUnique: vi.fn().mockResolvedValue(null), }, }); const caller = createManagerCaller(db); const result = await caller.reject({ id: "vac_1", rejectionReason: "Team conflict" }); expect(result.status).toBe(VacationStatus.REJECTED); expect(db.vacation.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ status: VacationStatus.REJECTED, rejectionReason: "Team conflict", }), }), ); }); it("throws when rejecting non-PENDING vacation", async () => { const db = createVacationDb({ vacation: { findUnique: vi.fn().mockResolvedValue({ ...sampleVacation, status: VacationStatus.APPROVED, }), }, }); const caller = createManagerCaller(db); await expect(caller.reject({ id: "vac_1" })).rejects.toThrow( "Only PENDING vacations can be rejected", ); }); }); describe("cancel", () => { it("cancels an existing vacation", async () => { const updatedVacation = { ...sampleVacation, status: VacationStatus.CANCELLED, }; const db = createVacationDb({ vacation: { findUnique: vi.fn().mockResolvedValue(sampleVacation), update: vi.fn().mockResolvedValue(updatedVacation), }, }); const caller = createProtectedCaller(db); const result = await caller.cancel({ id: "vac_1" }); expect(result.status).toBe(VacationStatus.CANCELLED); }); it("throws NOT_FOUND for missing vacation", async () => { const db = createVacationDb({ vacation: { findUnique: vi.fn().mockResolvedValue(null), }, }); const caller = createProtectedCaller(db); await expect(caller.cancel({ id: "missing" })).rejects.toThrow("Vacation not found"); }); it("throws when already cancelled", async () => { const db = createVacationDb({ vacation: { findUnique: vi.fn().mockResolvedValue({ ...sampleVacation, status: VacationStatus.CANCELLED, }), }, }); const caller = createProtectedCaller(db); await expect(caller.cancel({ id: "vac_1" })).rejects.toThrow("Already cancelled"); }); }); describe("batchApprove", () => { it("approves multiple pending vacations", async () => { const db = createVacationDb({ user: { findUnique: vi.fn().mockResolvedValue({ id: "mgr_1" }), }, vacation: { findMany: vi.fn().mockResolvedValue([ { id: "vac_1", resourceId: "res_1" }, { id: "vac_2", resourceId: "res_2" }, ]), updateMany: vi.fn().mockResolvedValue({ count: 2 }), }, resource: { findUnique: vi.fn().mockResolvedValue(null), }, }); const caller = createManagerCaller(db); const result = await caller.batchApprove({ ids: ["vac_1", "vac_2"] }); expect(result.approved).toBe(2); expect(db.vacation.updateMany).toHaveBeenCalledWith( expect.objectContaining({ where: { id: { in: ["vac_1", "vac_2"] } }, data: expect.objectContaining({ status: VacationStatus.APPROVED, }), }), ); }); it("only approves PENDING vacations from the requested set", async () => { const db = createVacationDb({ user: { findUnique: vi.fn().mockResolvedValue({ id: "mgr_1" }), }, vacation: { findMany: vi.fn().mockResolvedValue([ { id: "vac_1", resourceId: "res_1" }, ]), updateMany: vi.fn().mockResolvedValue({ count: 1 }), }, resource: { findUnique: vi.fn().mockResolvedValue(null), }, }); const caller = createManagerCaller(db); const result = await caller.batchApprove({ ids: ["vac_1", "vac_already_approved"] }); expect(result.approved).toBe(1); expect(db.vacation.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: { id: { in: ["vac_1", "vac_already_approved"] }, status: VacationStatus.PENDING }, }), ); }); }); describe("batchReject", () => { it("rejects multiple pending vacations with optional reason", async () => { const db = createVacationDb({ user: { findUnique: vi.fn().mockResolvedValue({ id: "mgr_1" }), }, vacation: { findMany: vi.fn().mockResolvedValue([ { id: "vac_1", resourceId: "res_1" }, ]), updateMany: vi.fn().mockResolvedValue({ count: 1 }), }, resource: { findUnique: vi.fn().mockResolvedValue(null), }, }); const caller = createManagerCaller(db); const result = await caller.batchReject({ ids: ["vac_1"], rejectionReason: "Budget freeze", }); expect(result.rejected).toBe(1); expect(db.vacation.updateMany).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ status: VacationStatus.REJECTED, rejectionReason: "Budget freeze", }), }), ); }); }); describe("getForResource", () => { it("returns approved vacations in date range", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_1" }), }, vacation: { findMany: vi.fn().mockResolvedValue([ { id: "vac_1", startDate: new Date("2026-06-01"), endDate: new Date("2026-06-05"), type: VacationType.ANNUAL, status: VacationStatus.APPROVED, }, ]), }, }; const caller = createProtectedCaller(db); const result = await caller.getForResource({ resourceId: "res_1", startDate: new Date("2026-01-01"), endDate: new Date("2026-12-31"), }); expect(result).toHaveLength(1); expect(db.vacation.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ resourceId: "res_1", status: VacationStatus.APPROVED, }), }), ); }); it("forbids regular users from reading another resource's approved vacations", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_own" }), }, vacation: { findMany: vi.fn(), }, }; const caller = createProtectedCaller(db); await expect( caller.getForResource({ resourceId: "res_other", startDate: new Date("2026-01-01"), endDate: new Date("2026-12-31"), }), ).rejects.toThrow("You can only view vacation data for your own resource"); expect(db.vacation.findMany).not.toHaveBeenCalled(); }); }); describe("getPendingApprovals", () => { it("returns all pending vacations for managers", async () => { const db = { vacation: { findMany: vi.fn().mockResolvedValue([sampleVacation]), }, }; const caller = createManagerCaller(db); const result = await caller.getPendingApprovals(); expect(result).toHaveLength(1); expect(db.vacation.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: { status: VacationStatus.PENDING }, }), ); }); it("forbids regular users from viewing pending approvals", async () => { const db = {}; const caller = createProtectedCaller(db); await expect(caller.getPendingApprovals()).rejects.toThrow(); }); }); describe("getTeamOverlap", () => { it("returns overlapping vacations for the same chapter", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_1" }), findUnique: vi.fn().mockResolvedValue({ chapter: "Animation" }), }, vacation: { findMany: vi.fn().mockResolvedValue([ { ...sampleVacation, id: "vac_other", resourceId: "res_2", resource: { id: "res_2", displayName: "Bob", eid: "E-002" }, }, ]), }, }; const caller = createProtectedCaller(db); const result = await caller.getTeamOverlap({ resourceId: "res_1", startDate: new Date("2026-06-01"), endDate: new Date("2026-06-05"), }); expect(result).toHaveLength(1); expect(db.vacation.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ resource: { chapter: "Animation" }, resourceId: { not: "res_1" }, }), }), ); }); it("returns empty array when resource has no chapter", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_1" }), findUnique: vi.fn().mockResolvedValue({ chapter: null }), }, }; const caller = createProtectedCaller(db); const result = await caller.getTeamOverlap({ resourceId: "res_1", startDate: new Date("2026-06-01"), endDate: new Date("2026-06-05"), }); expect(result).toEqual([]); }); it("forbids regular users from reading another resource's team overlap", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_own" }), findUnique: vi.fn(), }, vacation: { findMany: vi.fn(), }, }; const caller = createProtectedCaller(db); await expect( caller.getTeamOverlap({ resourceId: "res_other", startDate: new Date("2026-06-01"), endDate: new Date("2026-06-05"), }), ).rejects.toThrow("You can only view vacation data for your own resource"); expect(db.resource.findUnique).not.toHaveBeenCalled(); expect(db.vacation.findMany).not.toHaveBeenCalled(); }); }); describe("getTeamOverlapDetail", () => { it("returns assistant-friendly overlap detail from the canonical overlap query", async () => { const db = { resource: { findFirst: vi.fn().mockResolvedValue({ id: "res_1" }), findUnique: vi .fn() .mockResolvedValueOnce({ displayName: "Bruce Banner", chapter: "CGI" }), }, vacation: { findMany: vi.fn().mockResolvedValue([ { ...sampleVacation, id: "vac_other", resourceId: "res_2", status: VacationStatus.APPROVED, resource: { id: "res_2", displayName: "Clark Kent", eid: "E-002" }, }, ]), }, }; const caller = createProtectedCaller(db); const result = await caller.getTeamOverlapDetail({ resourceId: "res_1", startDate: new Date("2026-08-10T00:00:00.000Z"), endDate: new Date("2026-08-12T00:00:00.000Z"), }); expect(result).toEqual({ resource: "Bruce Banner", chapter: "CGI", period: "2026-08-10 to 2026-08-12", overlapCount: 1, overlappingVacations: [ { resource: "Clark Kent", type: VacationType.ANNUAL, status: VacationStatus.APPROVED, start: "2026-06-01", end: "2026-06-05", }, ], }); }); }); describe("batchCreatePublicHolidays", () => { it("creates public holidays for all active resources (admin only)", async () => { const db = { resource: { findMany: vi.fn().mockResolvedValue([ { id: "res_1", federalState: "BY", country: { code: "DE" }, metroCity: null }, { id: "res_2", federalState: "BY", country: { code: "DE" }, metroCity: null }, ]), }, user: { findUnique: vi.fn().mockResolvedValue({ id: "admin_1" }), }, vacation: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), findFirst: vi.fn().mockResolvedValue(null), create: vi.fn().mockResolvedValue({}), }, }; const caller = createAdminCaller(db); const result = await caller.batchCreatePublicHolidays({ year: 2026, federalState: "BY", }); expect(result.created).toBeGreaterThan(0); expect(result.resources).toBe(2); expect(db.vacation.create).toHaveBeenCalled(); }); it("skips already existing holidays", async () => { const db = { resource: { findMany: vi.fn().mockResolvedValue([ { id: "res_1", federalState: "BY", country: { code: "DE" }, metroCity: null }, ]), }, user: { findUnique: vi.fn().mockResolvedValue({ id: "admin_1" }), }, vacation: { findFirst: vi.fn().mockResolvedValue({ id: "existing" }), create: vi.fn(), }, }; const caller = createAdminCaller(db); const result = await caller.batchCreatePublicHolidays({ year: 2026, federalState: "BY", }); expect(result.created).toBe(0); expect(db.vacation.create).not.toHaveBeenCalled(); }); it("forbids non-admin users", async () => { const db = {}; const caller = createManagerCaller(db); await expect( caller.batchCreatePublicHolidays({ year: 2026 }), ).rejects.toThrow("Admin role required"); }); }); describe("updateStatus", () => { it("allows manager to approve via updateStatus", async () => { const updatedVacation = { ...sampleVacation, status: VacationStatus.APPROVED, }; const db = { vacation: { findUnique: vi.fn().mockResolvedValue(sampleVacation), update: vi.fn().mockResolvedValue(updatedVacation), }, user: { findUnique: vi.fn().mockResolvedValue({ id: "mgr_1", systemRole: "MANAGER" }), }, }; const caller = createManagerCaller(db); const result = await caller.updateStatus({ id: "vac_1", status: "APPROVED" }); expect(result.status).toBe(VacationStatus.APPROVED); }); it("allows any user to cancel via updateStatus", async () => { const updatedVacation = { ...sampleVacation, status: VacationStatus.CANCELLED, }; const db = { vacation: { findUnique: vi.fn().mockResolvedValue(sampleVacation), update: vi.fn().mockResolvedValue(updatedVacation), }, user: { findUnique: vi.fn().mockResolvedValue({ id: "user_1", systemRole: "USER" }), }, }; const caller = createProtectedCaller(db); const result = await caller.updateStatus({ id: "vac_1", status: "CANCELLED" }); expect(result.status).toBe(VacationStatus.CANCELLED); }); it("forbids non-manager from approving via updateStatus", async () => { const db = { vacation: { findUnique: vi.fn().mockResolvedValue(sampleVacation), }, user: { findUnique: vi.fn().mockResolvedValue({ id: "user_1", systemRole: "USER" }), }, }; const caller = createProtectedCaller(db); await expect( caller.updateStatus({ id: "vac_1", status: "APPROVED" }), ).rejects.toThrow("Manager role required to approve/reject"); }); it("throws NOT_FOUND when vacation does not exist", async () => { const db = { vacation: { findUnique: vi.fn().mockResolvedValue(null), }, }; const caller = createProtectedCaller(db); await expect( caller.updateStatus({ id: "missing", status: "CANCELLED" }), ).rejects.toThrow("Vacation not found"); }); }); });