import { BadRequestException, NotFoundException } from '@nestjs/common'; import { TrainSchedulingService } from './services/train-scheduling.service'; import type { UpdateScheduleTrainNumberDto } from './dto/update-schedule-train-number.dto'; /** * Guards around renumbering a departure. Exercised against a stub repository — * the rules (dispatch lock, empty-body rejection, clear-vs-leave semantics) are * pure service logic and need no database. */ describe('TrainSchedulingService.updateScheduleTrainNumber', () => { const makeService = (schedule: Record | null) => { const update = jest.fn().mockResolvedValue(undefined); const findById = jest.fn().mockResolvedValue(schedule); const service = Object.create( TrainSchedulingService.prototype, ) as TrainSchedulingService; Object.assign(service, { trainSchedulesRepository: { findById, update }, logger: { log: jest.fn(), warn: jest.fn() }, }); return { service, update, findById }; }; const call = (service: TrainSchedulingService, dto: UpdateScheduleTrainNumberDto) => service.updateScheduleTrainNumber('sched-1', dto); it('updates both numbers on a SCHEDULED train', async () => { const { service, update } = makeService({ id: 'sched-1', status: 'SCHEDULED', trainNumber: '9101', voyageNumber: null, }); await call(service, { trainNumber: '9201', voyageNumber: 'V-2026-014' }); expect(update).toHaveBeenCalledWith('sched-1', { trainNumber: '9201', voyageNumber: 'V-2026-014', }); }); it('refuses to renumber a dispatched train', async () => { // The numbers are already printed on paperwork that left with the train. const { service, update } = makeService({ id: 'sched-1', status: 'DISPATCHED', trainNumber: '9101', }); await expect(call(service, { trainNumber: '9201' })).rejects.toBeInstanceOf( BadRequestException, ); expect(update).not.toHaveBeenCalled(); }); it.each(['ARRIVED', 'CANCELLED', 'COMPLETED'])( 'refuses to renumber a %s schedule', async (status) => { const { service, update } = makeService({ id: 'sched-1', status }); await expect(call(service, { trainNumber: '9201' })).rejects.toBeInstanceOf( BadRequestException, ); expect(update).not.toHaveBeenCalled(); }, ); it('rejects a body carrying neither number before touching the schedule', async () => { const { service, update, findById } = makeService({ id: 'sched-1', status: 'DRAFT', }); await expect(call(service, {})).rejects.toBeInstanceOf(BadRequestException); expect(findById).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); }); it('leaves an omitted field untouched rather than clearing it', async () => { const { service, update } = makeService({ id: 'sched-1', status: 'DRAFT', trainNumber: '9101', voyageNumber: 'V-1', }); await call(service, { trainNumber: '9201' }); expect(update).toHaveBeenCalledWith('sched-1', { trainNumber: '9201' }); expect(update.mock.calls[0][1]).not.toHaveProperty('voyageNumber'); }); it('clears a field when an empty string is sent', async () => { const { service, update } = makeService({ id: 'sched-1', status: 'DRAFT', voyageNumber: 'V-1', }); await call(service, { voyageNumber: ' ' }); expect(update).toHaveBeenCalledWith('sched-1', { voyageNumber: null }); }); it('404s on an unknown schedule', async () => { const { service } = makeService(null); await expect(call(service, { trainNumber: '9201' })).rejects.toBeInstanceOf( NotFoundException, ); }); });