208 lines
6.5 KiB
TypeScript
208 lines
6.5 KiB
TypeScript
import { VacationStatus, VacationType } from "@capakraken/db";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
createVacationRequest,
|
|
CreateVacationRequestSchema,
|
|
} from "../router/vacation-create-support.js";
|
|
|
|
const {
|
|
emitVacationCreated,
|
|
createAuditEntry,
|
|
createVacationApprovalTasks,
|
|
getAnonymizationDirectory,
|
|
resolveVacationCreationChargeability,
|
|
} = vi.hoisted(() => ({
|
|
emitVacationCreated: vi.fn(),
|
|
createAuditEntry: vi.fn(),
|
|
createVacationApprovalTasks: vi.fn(),
|
|
getAnonymizationDirectory: vi.fn(),
|
|
resolveVacationCreationChargeability: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../sse/event-bus.js", () => ({
|
|
emitVacationCreated,
|
|
}));
|
|
|
|
vi.mock("../lib/audit.js", () => ({
|
|
createAuditEntry,
|
|
}));
|
|
|
|
vi.mock("../lib/anonymization.js", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("../lib/anonymization.js")>();
|
|
return {
|
|
...actual,
|
|
anonymizeResource: vi.fn((value) => value),
|
|
anonymizeUser: vi.fn((value) => value),
|
|
getAnonymizationDirectory,
|
|
};
|
|
});
|
|
|
|
vi.mock("../router/vacation-chargeability.js", () => ({
|
|
resolveVacationCreationChargeability,
|
|
}));
|
|
|
|
vi.mock("../router/vacation-side-effects.js", () => ({
|
|
createVacationApprovalTasks,
|
|
}));
|
|
|
|
function createContext(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
session: {
|
|
user: { email: "user@example.com", name: "User", image: null },
|
|
expires: "2099-01-01T00:00:00.000Z",
|
|
},
|
|
db: {
|
|
user: {
|
|
findUnique: vi.fn().mockResolvedValue({ id: "user_1", systemRole: "USER" }),
|
|
},
|
|
resource: {
|
|
findUnique: vi.fn().mockResolvedValue({ userId: "user_1" }),
|
|
},
|
|
vacation: {
|
|
findFirst: vi.fn().mockResolvedValue(null),
|
|
create: vi.fn().mockResolvedValue({
|
|
id: "vac_1",
|
|
resourceId: "res_1",
|
|
type: VacationType.ANNUAL,
|
|
status: VacationStatus.PENDING,
|
|
startDate: new Date("2026-06-01T00:00:00.000Z"),
|
|
endDate: new Date("2026-06-03T00:00:00.000Z"),
|
|
isHalfDay: false,
|
|
halfDayPart: null,
|
|
resource: { id: "res_1", displayName: "Alice", eid: "E-001" },
|
|
requestedBy: { id: "user_1", name: "User", email: "user@example.com" },
|
|
}),
|
|
},
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("vacation create support", () => {
|
|
beforeEach(() => {
|
|
emitVacationCreated.mockReset();
|
|
createAuditEntry.mockReset();
|
|
createVacationApprovalTasks.mockReset();
|
|
getAnonymizationDirectory.mockReset();
|
|
resolveVacationCreationChargeability.mockReset();
|
|
|
|
getAnonymizationDirectory.mockResolvedValue({
|
|
resourcesById: new Map(),
|
|
usersById: new Map(),
|
|
});
|
|
resolveVacationCreationChargeability.mockResolvedValue({
|
|
effectiveDays: 3,
|
|
deductionSnapshotWriteData: { deductedDays: 3, deductionSnapshot: { source: "calendar" } },
|
|
});
|
|
});
|
|
|
|
it("validates half-day requests against cross-day ranges", () => {
|
|
expect(() => CreateVacationRequestSchema.parse({
|
|
resourceId: "res_1",
|
|
type: VacationType.ANNUAL,
|
|
startDate: "2026-06-01",
|
|
endDate: "2026-06-02",
|
|
isHalfDay: true,
|
|
halfDayPart: "MORNING",
|
|
})).toThrowError(/Half-day requests must start and end on the same day/);
|
|
});
|
|
|
|
it("creates pending vacations for regular users and schedules approval tasks", async () => {
|
|
const ctx = createContext();
|
|
|
|
const result = await createVacationRequest(ctx as never, {
|
|
resourceId: "res_1",
|
|
type: VacationType.ANNUAL,
|
|
startDate: new Date("2026-06-01T00:00:00.000Z"),
|
|
endDate: new Date("2026-06-03T00:00:00.000Z"),
|
|
note: "Summer",
|
|
isHalfDay: false,
|
|
});
|
|
|
|
expect(ctx.db.vacation.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
data: expect.objectContaining({
|
|
status: VacationStatus.PENDING,
|
|
requestedById: "user_1",
|
|
deductedDays: 3,
|
|
}),
|
|
}));
|
|
expect(createVacationApprovalTasks).toHaveBeenCalledWith(expect.objectContaining({
|
|
submittedByUserId: "user_1",
|
|
vacationId: "vac_1",
|
|
}));
|
|
expect(emitVacationCreated).toHaveBeenCalledWith({
|
|
id: "vac_1",
|
|
resourceId: "res_1",
|
|
status: VacationStatus.PENDING,
|
|
});
|
|
expect(result).toMatchObject({
|
|
id: "vac_1",
|
|
effectiveDays: 3,
|
|
});
|
|
});
|
|
|
|
it("auto-approves manager-created vacations without approval tasks", async () => {
|
|
const ctx = createContext({
|
|
db: {
|
|
user: {
|
|
findUnique: vi.fn().mockResolvedValue({ id: "mgr_1", systemRole: "MANAGER" }),
|
|
},
|
|
resource: {
|
|
findUnique: vi.fn(),
|
|
},
|
|
vacation: {
|
|
findFirst: vi.fn().mockResolvedValue(null),
|
|
create: vi.fn().mockResolvedValue({
|
|
id: "vac_mgr",
|
|
resourceId: "res_2",
|
|
type: VacationType.ANNUAL,
|
|
status: VacationStatus.APPROVED,
|
|
startDate: new Date("2026-07-01T00:00:00.000Z"),
|
|
endDate: new Date("2026-07-02T00:00:00.000Z"),
|
|
isHalfDay: false,
|
|
halfDayPart: null,
|
|
resource: { id: "res_2", displayName: "Bob", eid: "E-002" },
|
|
requestedBy: { id: "mgr_1", name: "Manager", email: "user@example.com" },
|
|
}),
|
|
},
|
|
},
|
|
});
|
|
|
|
await createVacationRequest(ctx as never, {
|
|
resourceId: "res_2",
|
|
type: VacationType.ANNUAL,
|
|
startDate: new Date("2026-07-01T00:00:00.000Z"),
|
|
endDate: new Date("2026-07-02T00:00:00.000Z"),
|
|
isHalfDay: false,
|
|
});
|
|
|
|
expect(ctx.db.resource.findUnique).not.toHaveBeenCalled();
|
|
expect(ctx.db.vacation.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
data: expect.objectContaining({
|
|
status: VacationStatus.APPROVED,
|
|
approvedById: "mgr_1",
|
|
}),
|
|
}));
|
|
expect(createVacationApprovalTasks).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects manual public holiday creation before hitting the database", async () => {
|
|
const ctx = createContext();
|
|
|
|
await expect(createVacationRequest(ctx as never, {
|
|
resourceId: "res_1",
|
|
type: VacationType.PUBLIC_HOLIDAY,
|
|
startDate: new Date("2026-12-25T00:00:00.000Z"),
|
|
endDate: new Date("2026-12-25T00:00:00.000Z"),
|
|
isHalfDay: false,
|
|
})).rejects.toThrowError(new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "Public holidays must be managed via Holiday Calendars or the legacy holiday import, not via manual vacation requests",
|
|
}));
|
|
|
|
expect(ctx.db.user.findUnique).not.toHaveBeenCalled();
|
|
expect(ctx.db.vacation.create).not.toHaveBeenCalled();
|
|
});
|
|
});
|