import { describe, it, expect, beforeAll, afterAll } from "vitest"; import Fastify from "fastify"; import jwt from "jsonwebtoken"; import prisma from "../config/database"; import { config } from "../config/env"; import leaveRequestsRoutes from "../routes/admin/leave-requests"; /** * Pinning tests for audit finding H6 + the year-spanning fast-follow: * leave-request creation and approval must mirror attendance.service * createLeave — skip Czech public holidays (a weekday holiday is already * paid/free; charging it double-deducts) and book each day's hours against * ITS OWN calendar year's balance (a Dec→Jan request used to charge * everything to the start year). * * Fixture math (verified): 2098-05-01 (Thu) and 2098-05-08 (Thu) are both * weekday holidays → the 05-01..05-08 range has 4 chargeable days (32h), * not 6 (48h). 2098-12-28 (Sun)..2099-01-05 (Mon) has 3 chargeable days in * 2098 (29/30/31 = 24h) and 2 in 2099 (Jan 2 + Jan 5 = 16h; Jan 1 is a * holiday). */ const N = "leave_holiday_"; let app: ReturnType; let adminToken: string; let userToken: string; let userId: number; async function cleanup() { const users = await prisma.users.findMany({ where: { username: { startsWith: N } }, select: { id: true }, }); const ids = users.map((u) => u.id); if (ids.length > 0) { await prisma.attendance.deleteMany({ where: { user_id: { in: ids } } }); await prisma.leave_requests.deleteMany({ where: { user_id: { in: ids } }, }); await prisma.leave_balances.deleteMany({ where: { user_id: { in: ids } } }); await prisma.users.deleteMany({ where: { id: { in: ids } } }); } } beforeAll(async () => { await cleanup(); app = Fastify({ logger: false }); await app.register(leaveRequestsRoutes, { prefix: "/api/admin/leave-requests", }); const admin = await prisma.users.findFirst({ where: { roles: { name: "admin" } }, }); if (!admin) throw new Error("Test setup: admin user not found"); adminToken = jwt.sign( { sub: admin.id, username: admin.username, role: "admin" }, config.jwt.secret, { expiresIn: "15m" }, ); const role = await prisma.roles.findFirst(); const user = await prisma.users.create({ data: { username: `${N}user`, email: `${N}user@test.local`, password_hash: "$2a$10$invalidinvalidinvalidinvalidinvalidinvalidinvalidinvali", first_name: "Leave", last_name: "Holiday", is_active: true, role_id: role!.id, }, }); userId = user.id; userToken = jwt.sign( { sub: user.id, username: user.username, role: role!.name }, config.jwt.secret, { expiresIn: "15m" }, ); for (const year of [2098, 2099]) { await prisma.leave_balances.create({ data: { user_id: userId, year, vacation_total: 200, vacation_used: 0, sick_used: 0, }, }); } }); afterAll(async () => { if (app) await app.close(); await cleanup(); await prisma.$disconnect(); }); async function balance(year: number) { return prisma.leave_balances.findFirst({ where: { user_id: userId, year }, }); } describe("leave requests vs Czech holidays (audit H6)", () => { it("creation excludes weekday public holidays from the charged total", async () => { const res = await app.inject({ method: "POST", url: "/api/admin/leave-requests", headers: { Authorization: `Bearer ${userToken}`, "Content-Type": "application/json", }, payload: { leave_type: "vacation", date_from: "2098-05-01", date_to: "2098-05-08", }, }); expect(res.statusCode).toBe(201); const row = await prisma.leave_requests.findFirst({ where: { id: res.json().data.id }, }); expect(row?.total_days).toBe(4); expect(Number(row?.total_hours)).toBe(32); // Leave it cancelled so the approval test's range stays free. await prisma.leave_requests.update({ where: { id: row!.id }, data: { status: "cancelled" }, }); }); it("approval charges only non-holiday business days and skips holiday attendance rows", async () => { // Stored totals mimic a PRE-FIX request (weekend-only counting) — the // approval must recompute, not trust them. const req = await prisma.leave_requests.create({ data: { user_id: userId, leave_type: "vacation", date_from: new Date(Date.UTC(2098, 4, 1)), date_to: new Date(Date.UTC(2098, 4, 8)), total_hours: 48, total_days: 6, status: "pending", }, }); const res = await app.inject({ method: "PUT", url: `/api/admin/leave-requests/${req.id}`, headers: { Authorization: `Bearer ${adminToken}`, "Content-Type": "application/json", }, payload: { status: "approved" }, }); expect(res.statusCode).toBe(200); expect(Number((await balance(2098))?.vacation_used)).toBe(32); const rows = await prisma.attendance.findMany({ where: { user_id: userId, leave_type: "vacation" }, }); const days = rows.map((r) => r.shift_date.toISOString().slice(0, 10)); expect(rows).toHaveLength(4); expect(days).not.toContain("2098-05-01"); expect(days).not.toContain("2098-05-08"); }); it("a year-spanning approval books each day against its own year's balance", async () => { const req = await prisma.leave_requests.create({ data: { user_id: userId, leave_type: "sick", date_from: new Date(Date.UTC(2098, 11, 28)), date_to: new Date(Date.UTC(2099, 0, 5)), total_hours: 48, total_days: 6, status: "pending", }, }); const res = await app.inject({ method: "PUT", url: `/api/admin/leave-requests/${req.id}`, headers: { Authorization: `Bearer ${adminToken}`, "Content-Type": "application/json", }, payload: { status: "approved" }, }); expect(res.statusCode).toBe(200); // 29/30/31 Dec 2098 = 24h; Jan 2 + Jan 5 2099 = 16h (Jan 1 is a holiday). expect(Number((await balance(2098))?.sick_used)).toBe(24); expect(Number((await balance(2099))?.sick_used)).toBe(16); }); });