import { BadRequestException, NotFoundException } from '@nestjs/common'; import { ExpensesService } from './expenses.service'; import { PersonalExpense } from '../entities/personal-expense.entity'; const qb = (affected = 1) => ({ delete: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), execute: jest.fn().mockResolvedValue({ affected }), }); function createService(options?: { roomFind?: any[]; personalFind?: any[]; roomExpense?: any; personalExpense?: any; }) { const roomExpRepo = { create: jest.fn((value) => value), save: jest.fn(async (value) => value), find: jest.fn().mockResolvedValue(options?.roomFind ?? []), findOne: jest.fn().mockResolvedValue(options?.roomExpense ?? null), createQueryBuilder: jest.fn(() => qb()), }; const personalExpRepo = { create: jest.fn((value) => value), save: jest.fn(async (value) => value), find: jest.fn().mockResolvedValue(options?.personalFind ?? []), findOne: jest.fn().mockResolvedValue(options?.personalExpense ?? null), createQueryBuilder: jest.fn(() => qb()), delete: jest.fn(), }; const roomRepo = { find: jest.fn().mockImplementation(async () => options?.roomFind ?? []), findOne: jest.fn().mockResolvedValue({ id: 1 }), }; const studentRepo = { findOne: jest.fn().mockResolvedValue({ id: 1 }) }; return { service: new ExpensesService(roomExpRepo as any, personalExpRepo as any, roomRepo as any, studentRepo as any, {} as any), roomExpRepo, personalExpRepo, roomRepo, }; } describe('ExpensesService boundaries', () => { it('rejects an empty room-expense batch', async () => { const { service, roomExpRepo } = createService(); await expect(service.batchCreateRoomExpenses({ periodStart: '2026-07-01', periodEnd: '2026-07-31', expenses: [], })).rejects.toBeInstanceOf(BadRequestException); expect(roomExpRepo.save).not.toHaveBeenCalled(); }); it('rejects a batch when any room does not exist', async () => { const { service, roomExpRepo } = createService({ roomFind: [{ id: 1 }] }); await expect(service.batchCreateRoomExpenses({ periodStart: '2026-07-01', periodEnd: '2026-07-31', expenses: [ { roomId: 1, expenseType: 'water', amount: 10 }, { roomId: 2, expenseType: 'water', amount: 20 }, ], })).rejects.toBeInstanceOf(NotFoundException); expect(roomExpRepo.save).not.toHaveBeenCalled(); }); it('rejects a reversed room-expense period', async () => { const { service } = createService(); await expect(service.createRoomExpense({ roomId: 1, expenseType: 'water', amount: 10, periodStart: '2026-08-01', periodEnd: '2026-07-31', })).rejects.toBeInstanceOf(BadRequestException); }); it('rejects a batch delete when only part of the ids exist', async () => { const { service, roomExpRepo } = createService({ roomFind: [{ id: 1 }] }); await expect(service.batchDeleteRoomExpenses([1, 2])).rejects.toBeInstanceOf(NotFoundException); expect(roomExpRepo.createQueryBuilder).not.toHaveBeenCalled(); }); it('does not edit or delete a personal expense already linked to a bill', async () => { const linked = { id: 1, studentId: 1, amount: 20, billId: 9 } as PersonalExpense; const { service, personalExpRepo } = createService({ personalExpense: linked }); await expect(service.updatePersonalExpense(1, { amount: 30 })).rejects.toBeInstanceOf(BadRequestException); await expect(service.deletePersonalExpense(1)).rejects.toBeInstanceOf(BadRequestException); expect(personalExpRepo.save).not.toHaveBeenCalled(); expect(personalExpRepo.delete).not.toHaveBeenCalled(); }); it('rejects a personal-expense batch delete containing billed records', async () => { const { service, personalExpRepo } = createService({ personalFind: [ { id: 1, billId: null }, { id: 2, billId: 9 }, ], }); await expect(service.batchDeletePersonalExpenses([1, 2])).rejects.toBeInstanceOf(BadRequestException); expect(personalExpRepo.createQueryBuilder).not.toHaveBeenCalled(); }); });