test: harden business boundary conditions

This commit is contained in:
2026-07-15 00:03:55 +08:00
parent 17a5046ea0
commit b1f35f9d1a
65 changed files with 2311 additions and 293 deletions

View File

@@ -0,0 +1,77 @@
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<Bill>[] = []) {
const billRepo = {
find: jest.fn().mockResolvedValue(bills),
findOne: jest.fn().mockResolvedValue(bills[0] ?? null),
save: jest.fn(async (value) => value),
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 delete', async () => {
const { service, dataSource } = createService();
await expect(service.batchRemove([])).rejects.toBeInstanceOf(BadRequestException);
expect(dataSource.transaction).not.toHaveBeenCalled();
});
it('rejects a batch delete 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('deletes a bill and its links in one transaction', async () => {
const { service, dataSource, manager } = createService([{ id: 1, paidAmount: 0, status: 'unpaid' }]);
await expect(service.remove(1)).resolves.toEqual({ message: '账单已删除' });
expect(dataSource.transaction).toHaveBeenCalledTimes(1);
expect(manager.delete).toHaveBeenCalledTimes(2);
expect(manager.update).toHaveBeenCalledTimes(1);
});
});