forked from wangziqi/gongxue-base
test: harden business boundary conditions
This commit is contained in:
107
apps/server/src/expenses/expenses.boundaries.spec.ts
Normal file
107
apps/server/src/expenses/expenses.boundaries.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user