import { beforeEach, describe, expect, it, vi } from 'vitest'; import { prisma } from '@/lib/db'; import type { Slot } from '@/lib/schedule/types'; import { toDraft, type WeekDraft } from '@/lib/schedule/week'; import { hasDatabase, resetDatabase } from './helpers'; // The real module pulls in NextAuth, which drags half of Next into a test that // only cares about what reaches the database. 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 { saveWeek } = await import('@/app/admin/horaires/actions'); const FULL: Slot[] = [{ open: '10:00', close: '18:30' }]; async function storedWeek(): Promise { const rows = await prisma.weeklySchedule.findMany(); return toDraft( rows.map((row) => ({ dayOfWeek: row.dayOfWeek, isClosed: row.isClosed, slots: (row.slots as Slot[] | null) ?? [], })), ); } function withDay(week: WeekDraft, dayOfWeek: number, patch: Partial): WeekDraft { return week.map((day) => (day.dayOfWeek === dayOfWeek ? { ...day, ...patch } : day)); } describe.skipIf(!hasDatabase)('saveWeek', () => { beforeEach(async () => { gate.result = { ok: true, value: caller }; await resetDatabase(); await prisma.auditLog.deleteMany(); }); it('persists a changed day and names it in the result', async () => { const week = withDay(await storedWeek(), 1, { isClosed: false, slots: FULL }); const result = await saveWeek(week); expect(result).toEqual({ ok: true, changed: [1] }); const monday = await prisma.weeklySchedule.findUnique({ where: { dayOfWeek: 1 } }); expect(monday?.isClosed).toBe(false); expect(monday?.slots).toEqual(FULL); }); it('writes one readable audit entry', async () => { await saveWeek(withDay(await storedWeek(), 1, { isClosed: false, slots: FULL })); const entries = await prisma.auditLog.findMany(); expect(entries).toHaveLength(1); expect(entries[0]).toMatchObject({ userEmail: caller.email, action: 'weekly_schedule.update', entity: 'WeeklySchedule', }); // The diff names the day in French and shows both states, so the log can // be read without cross-referencing the schema. expect(entries[0]?.diff).toEqual({ Lundi: { before: 'Fermé', after: '10:00–18:30' }, }); }); it('writes nothing at all when nothing moved', async () => { const result = await saveWeek(await storedWeek()); expect(result).toEqual({ ok: true, changed: [] }); // No audit entry either: a log full of empty saves is a log nobody reads. expect(await prisma.auditLog.count()).toBe(0); }); it('ignores a reordering that changes nothing', async () => { const split: Slot[] = [ { open: '10:00', close: '13:00' }, { open: '14:00', close: '18:30' }, ]; await saveWeek(withDay(await storedWeek(), 2, { isClosed: false, slots: split })); await prisma.auditLog.deleteMany(); const reordered = withDay(await storedWeek(), 2, { isClosed: false, slots: [split[1]!, split[0]!], }); expect(await saveWeek(reordered)).toEqual({ ok: true, changed: [] }); }); it('refuses overlapping slots and does not touch the database', async () => { const before = await storedWeek(); const result = await saveWeek( withDay(before, 2, { isClosed: false, slots: [ { open: '10:00', close: '14:00' }, { open: '13:00', close: '18:30' }, ], }), ); expect(result.ok).toBe(false); if (!result.ok) { expect(result.dayErrors?.[2]?.join(' ')).toMatch(/chevauchent/); } expect(await storedWeek()).toEqual(before); }); it('refuses an open day with no hours', async () => { const result = await saveWeek( withDay(await storedWeek(), 2, { isClosed: false, slots: [] }), ); expect(result.ok).toBe(false); }); it('drops the leftover slots of a day being closed', async () => { await saveWeek(withDay(await storedWeek(), 2, { isClosed: true, slots: FULL })); const tuesday = await prisma.weeklySchedule.findUnique({ where: { dayOfWeek: 2 } }); expect(tuesday?.isClosed).toBe(true); expect(tuesday?.slots).toEqual([]); }); it('refuses a read-only account and leaves the week alone', async () => { const before = await storedWeek(); gate.result = { ok: false, error: 'Ce compte est en lecture seule.' } as never; const result = await saveWeek(withDay(before, 1, { isClosed: false, slots: FULL })); expect(result.ok).toBe(false); expect(await storedWeek()).toEqual(before); expect(await prisma.auditLog.count()).toBe(0); }); });