import { beforeEach, describe, expect, it, vi } from 'vitest'; import { prisma } from '@/lib/db'; import { hasDatabase, resetDatabase } from './helpers'; const caller = { email: 'test@ita-ito.test', role: 'admin' as const }; const gate = vi.hoisted(() => ({ result: { ok: true, value: { email: 'test@ita-ito.test', role: 'admin' as const } }, })); vi.mock('@/lib/auth/actions', () => ({ requireAdmin: async () => gate.result })); vi.mock('next/cache', () => ({ revalidatePath: () => undefined })); const { createVacation, deleteVacation } = await import('@/app/admin/vacances/actions'); describe.skipIf(!hasDatabase)('vacation actions', () => { beforeEach(async () => { gate.result = { ok: true, value: caller }; await resetDatabase(); await prisma.auditLog.deleteMany(); }); it('creates a period and records who did it', async () => { const result = await createVacation({ startDate: '2026-07-20', endDate: '2026-08-10', labelFr: ' Congés d’été ', }); // The id comes back so the caller can translate the label without having // to ask which row was "the last one". expect(result).toMatchObject({ ok: true }); if (result.ok) { expect(result.id).toEqual(expect.any(String)); } const stored = await prisma.vacationPeriod.findMany(); expect(stored).toHaveLength(1); // The label is trimmed: it goes straight onto the shop window. expect(stored[0]?.labelFr).toBe('Congés d’été'); expect(stored[0]?.createdBy).toBe(caller.email); const entry = await prisma.auditLog.findFirst(); expect(entry).toMatchObject({ action: 'vacation.create', entity: 'VacationPeriod' }); expect(entry?.diff).toMatchObject({ jours: { after: 22 } }); }); it('refuses a period overlapping an existing one, naming it', async () => { await createVacation({ startDate: '2026-07-20', endDate: '2026-08-10', labelFr: 'Été' }); const result = await createVacation({ startDate: '2026-08-05', endDate: '2026-08-20', labelFr: 'Encore', }); expect(result.ok).toBe(false); if (!result.ok) { // Two periods covering one day would both be in effect with no way to // say which, so the message has to name the collision. expect(result.error).toContain('Été'); } expect(await prisma.vacationPeriod.count()).toBe(1); }); it('accepts a period starting the day after another ends', async () => { await createVacation({ startDate: '2026-07-20', endDate: '2026-08-10', labelFr: 'Été' }); const result = await createVacation({ startDate: '2026-08-11', endDate: '2026-08-20', labelFr: 'Suite', }); expect(result).toMatchObject({ ok: true }); expect(await prisma.vacationPeriod.count()).toBe(2); }); it('refuses a reversed range and an empty label', async () => { expect( (await createVacation({ startDate: '2026-08-10', endDate: '2026-07-20', labelFr: 'X' })).ok, ).toBe(false); expect( (await createVacation({ startDate: '2026-07-20', endDate: '2026-08-10', labelFr: ' ' })).ok, ).toBe(false); expect(await prisma.vacationPeriod.count()).toBe(0); }); it('deletes a period and logs what it was', async () => { await createVacation({ startDate: '2026-07-20', endDate: '2026-08-10', labelFr: 'Été' }); const created = await prisma.vacationPeriod.findFirstOrThrow(); await prisma.auditLog.deleteMany(); expect(await deleteVacation(created.id)).toEqual({ ok: true }); expect(await prisma.vacationPeriod.count()).toBe(0); const entry = await prisma.auditLog.findFirst(); expect(entry?.action).toBe('vacation.delete'); expect(entry?.diff).toMatchObject({ libellé: { before: 'Été', after: null } }); }); it('reports a period that has already gone', async () => { expect(await deleteVacation('does-not-exist')).toEqual({ ok: false, error: 'Cette période a déjà été supprimée.', }); }); it('refuses a read-only account', async () => { gate.result = { ok: false, error: 'Ce compte est en lecture seule.' } as never; const result = await createVacation({ startDate: '2026-07-20', endDate: '2026-08-10', labelFr: 'Été', }); expect(result.ok).toBe(false); expect(await prisma.vacationPeriod.count()).toBe(0); expect(await prisma.auditLog.count()).toBe(0); }); });