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,107 @@
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();
});
});