import { describe, it, expect, vi, beforeEach } from 'vitest'; vi.mock('$lib/db', () => ({ prisma: { schedule: { findUnique: vi.fn(), create: vi.fn(), update: vi.fn() }, licenseKey: { findUnique: vi.fn() }, block: { findMany: vi.fn(), findFirst: vi.fn(), create: vi.fn(), createMany: vi.fn(), update: vi.fn(), delete: vi.fn(), deleteMany: vi.fn() }, blockProclamations: { createMany: vi.fn() } } })); import { prisma } from '$lib/db'; import { createBlock, deleteBlock, getOrCreateScheduleForLicense, listBlocksForLicense, setScheduleType, updateBlock } from './schedule'; const scheduleFindUnique = prisma.schedule.findUnique as ReturnType; const scheduleCreate = prisma.schedule.create as ReturnType; const scheduleUpdate = prisma.schedule.update as ReturnType; const blockFindMany = prisma.block.findMany as ReturnType; const blockFindFirst = prisma.block.findFirst as ReturnType; const blockCreate = prisma.block.create as ReturnType; const blockUpdate = prisma.block.update as ReturnType; const blockDelete = prisma.block.delete as ReturnType; const blockDeleteMany = prisma.block.deleteMany as ReturnType; const validInput = { startMinutes: 6 * 60, endMinutes: 8 * 60, proclamationIds: ['p1'] }; beforeEach(() => { vi.clearAllMocks(); }); describe('getOrCreateScheduleForLicense', () => { it('returns the existing schedule', async () => { scheduleFindUnique.mockResolvedValue({ id: 's1', licenseKeyId: 'lk1' }); const result = await getOrCreateScheduleForLicense('lk1'); expect(result).toEqual({ id: 's1', licenseKeyId: 'lk1' }); expect(scheduleCreate).not.toHaveBeenCalled(); }); it('creates a schedule keyed by license when missing', async () => { scheduleFindUnique.mockResolvedValue(null); scheduleCreate.mockResolvedValue({ id: 's1', licenseKeyId: 'lk1' }); await getOrCreateScheduleForLicense('lk1'); expect(scheduleCreate).toHaveBeenCalledWith({ data: { licenseKeyId: 'lk1' } }); }); }); describe('listBlocksForLicense', () => { it('scopes to the license key', async () => { blockFindMany.mockResolvedValue([]); await listBlocksForLicense('lk1'); expect(blockFindMany).toHaveBeenCalledWith( expect.objectContaining({ where: { schedule: { licenseKeyId: 'lk1' } }, include: expect.objectContaining({ proclamations: expect.anything() }) }) ); }); }); describe('setScheduleType', () => { beforeEach(() => { scheduleFindUnique.mockResolvedValue({ id: 's1', licenseKeyId: 'lk1', type: null }); }); it('set type when empty', async () => { blockFindMany.mockResolvedValue([]); scheduleUpdate.mockResolvedValue({}); const result = await setScheduleType('lk1', 'daily'); expect(result).toEqual({ ok: true }); expect(scheduleUpdate).toHaveBeenCalledWith({ where: { id: 's1' }, data: { type: 'daily' } }); }); it('requires confirmation to switch a populated schedule', async () => { scheduleFindUnique.mockResolvedValue({ id: 's1', licenseKeyId: 'lk1', type: 'daily' }); blockFindMany.mockResolvedValue([ { id: 'b1', startMinutes: 0, endMinutes: 60, dynamicAudio: true, proclamations: [] } ]); const result = await setScheduleType('lk1', 'weekly'); expect(result).toEqual({ ok: false, error: 'confirm' }); expect(scheduleUpdate).not.toHaveBeenCalled(); }); it('clearing to null deletes the blocks', async () => { scheduleFindUnique.mockResolvedValue({ id: 's1', licenseKeyId: 'lk1', type: 'daily' }); blockFindMany.mockResolvedValue([]); blockDeleteMany.mockResolvedValue({ count: 0 }); scheduleUpdate.mockResolvedValue({}); const result = await setScheduleType('lk1', null, { confirmed: true }); expect(result).toEqual({ ok: true }); expect(blockDeleteMany).toHaveBeenCalledWith({ where: { scheduleId: 's1' } }); expect(scheduleUpdate).toHaveBeenCalledWith({ where: { id: 's1' }, data: { type: null } }); }); }); describe('createBlock', () => { beforeEach(() => { scheduleFindUnique.mockResolvedValue({ id: 's1', licenseKeyId: 'lk1', type: 'daily' }); }); it('creates a block with deduped proclamations in order', async () => { blockFindMany.mockResolvedValue([]); blockCreate.mockResolvedValue({ id: 'new-block' }); const result = await createBlock('lk1', { ...validInput, proclamationIds: ['p1', 'p1', 'p2'] }); expect(result).toEqual({ ok: true, id: 'new-block' }); expect(blockCreate).toHaveBeenCalledWith({ data: { scheduleId: 's1', day: null, startMinutes: validInput.startMinutes, endMinutes: validInput.endMinutes, dynamicAudio: true, proclamations: { create: [ { proclamationId: 'p1', sortOrder: 0 }, { proclamationId: 'p2', sortOrder: 1 } ] } } }); }); it('rejects when no type is set', async () => { scheduleFindUnique.mockResolvedValue({ id: 's1', licenseKeyId: 'lk1', type: null }); const result = await createBlock('lk1', validInput); expect(result).toEqual({ ok: false, error: 'Set a schedule type first.' }); expect(blockCreate).not.toHaveBeenCalled(); }); it('rejects an overlapping block with a specific message', async () => { blockFindMany.mockResolvedValue([ { id: 'a', day: null, startMinutes: 7 * 60, endMinutes: 9 * 60, proclamations: [] } ]); const result = await createBlock('lk1', validInput); expect(result).toEqual({ ok: false, error: 'This overlaps with your 7:00 AM–9:00 AM block.' }); expect(blockCreate).not.toHaveBeenCalled(); }); it('rejects an empty block before creating', async () => { blockFindMany.mockResolvedValue([]); const result = await createBlock('lk1', { ...validInput, proclamationIds: [] }); expect(result).toEqual({ ok: false, error: 'Select at least one proclamation.' }); expect(blockCreate).not.toHaveBeenCalled(); }); }); describe('updateBlock', () => { it('returns not found when the block does not belong to the license', async () => { blockFindFirst.mockResolvedValue(null); const result = await updateBlock('lk1', 'missing', validInput); expect(result).toEqual({ ok: false, error: 'Block not found.' }); expect(blockUpdate).not.toHaveBeenCalled(); }); it('replaces proclamations and excludes itself from overlap checks', async () => { blockFindFirst.mockResolvedValue({ id: 'b1', scheduleId: 's1' }); scheduleFindUnique.mockResolvedValue({ id: 's1', licenseKeyId: 'lk1', type: 'daily' }); blockFindMany.mockResolvedValue([]); blockUpdate.mockResolvedValue({ id: 'b1' }); const result = await updateBlock('lk1', 'b1', validInput); expect(result).toEqual({ ok: true, id: 'b1' }); expect(blockFindMany).toHaveBeenCalledWith( expect.objectContaining({ where: { scheduleId: 's1', id: { not: 'b1' } } }) ); expect(blockUpdate).toHaveBeenCalledWith({ where: { id: 'b1' }, data: { day: null, startMinutes: validInput.startMinutes, endMinutes: validInput.endMinutes, dynamicAudio: true, proclamations: { deleteMany: {}, create: [{ proclamationId: 'p1', sortOrder: 0 }] } } }); }); }); describe('deleteBlock', () => { it('returns not found when the block does not belong to the license', async () => { blockFindFirst.mockResolvedValue(null); const result = await deleteBlock('lk1', 'missing'); expect(result).toEqual({ ok: false, error: 'Block not found.' }); expect(blockDelete).not.toHaveBeenCalled(); }); it('deletes an owned block', async () => { blockFindFirst.mockResolvedValue({ id: 'b1' }); blockDelete.mockResolvedValue({ id: 'b1' }); const result = await deleteBlock('lk1', 'b1'); expect(result).toEqual({ ok: true, id: 'b1' }); expect(blockDelete).toHaveBeenCalledWith({ where: { id: 'b1' } }); }); });