import { BadRequestException, NotFoundException } from '@nestjs/common'; import { BillsService } from './bills.service'; import { Bill } from '../entities/bill.entity'; function queryBuilder() { return { update: jest.fn().mockReturnThis(), set: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), execute: jest.fn().mockResolvedValue({ affected: 1 }), }; } function createService(bills: Partial[] = []) { const billRepo = { find: jest.fn().mockResolvedValue(bills), findOne: jest.fn().mockResolvedValue(bills[0] ?? null), save: jest.fn(async (value) => value), update: jest.fn(), createQueryBuilder: jest.fn(() => queryBuilder()), }; const manager = { delete: jest.fn(), update: jest.fn(), }; const dataSource = { transaction: jest.fn(async (callback) => callback(manager)) }; const service = new BillsService( billRepo as any, { delete: jest.fn() } as any, {} as any, { update: jest.fn() } as any, {} as any, {} as any, dataSource as any, {} as any, ); return { service, billRepo, dataSource, manager }; } describe('BillsService state and batch boundaries', () => { it('rejects an empty batch status update', async () => { const { service, billRepo } = createService(); await expect(service.batchUpdateStatus([], 'paid')).rejects.toBeInstanceOf(BadRequestException); expect(billRepo.find).not.toHaveBeenCalled(); }); it('rejects a batch status update when some ids do not exist', async () => { const { service, billRepo } = createService([{ id: 1, paidAmount: 0, outstandingAmount: 10 }]); await expect(service.batchUpdateStatus([1, 2], 'unpaid')).rejects.toBeInstanceOf(NotFoundException); expect(billRepo.createQueryBuilder).not.toHaveBeenCalled(); }); it('rejects marking a partially paid bill unpaid', async () => { const { service, billRepo } = createService([{ id: 1, paidAmount: 10, outstandingAmount: 90, status: 'partially_paid' }]); await expect(service.updateStatus(1, { status: 'unpaid' })).rejects.toBeInstanceOf(BadRequestException); expect(billRepo.save).not.toHaveBeenCalled(); }); it('rejects an empty batch archive', async () => { const { service, dataSource } = createService(); await expect(service.batchRemove([])).rejects.toBeInstanceOf(BadRequestException); expect(dataSource.transaction).not.toHaveBeenCalled(); }); it('rejects a batch archive when some ids do not exist', async () => { const { service, dataSource } = createService([{ id: 1, paidAmount: 0, status: 'unpaid' }]); await expect(service.batchRemove([1, 2])).rejects.toBeInstanceOf(NotFoundException); expect(dataSource.transaction).not.toHaveBeenCalled(); }); it('archives an unpaid bill without deleting rows', async () => { const { service, billRepo, dataSource, manager } = createService([{ id: 1, paidAmount: 0, status: 'unpaid' }]); await expect(service.remove(1)).resolves.toEqual({ message: '账单已归档' }); expect(billRepo.save).not.toHaveBeenCalled(); expect(billRepo.createQueryBuilder).not.toHaveBeenCalled(); expect(billRepo.findOne).toHaveBeenCalledWith({ where: { id: 1 } }); expect((billRepo as any).update).toHaveBeenCalledWith(1, expect.objectContaining({ status: 'cancelled' })); expect(dataSource.transaction).not.toHaveBeenCalled(); expect(manager.delete).not.toHaveBeenCalled(); }); });