forked from wangziqi/gongxue-base
308 lines
12 KiB
TypeScript
308 lines
12 KiB
TypeScript
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
|
import { ExpensesService } from '../expenses/expenses.service';
|
|
import { OccupanciesService } from '../occupancies/occupancies.service';
|
|
import { RoomsService } from '../rooms/rooms.service';
|
|
import { StudentsService } from '../students/students.service';
|
|
|
|
function updateQb(affected = 1) {
|
|
const qb = {
|
|
update: jest.fn(),
|
|
set: jest.fn(),
|
|
where: jest.fn(),
|
|
execute: jest.fn().mockResolvedValue({ affected }),
|
|
};
|
|
qb.update.mockReturnValue(qb);
|
|
qb.set.mockReturnValue(qb);
|
|
qb.where.mockReturnValue(qb);
|
|
return qb;
|
|
}
|
|
|
|
function listQb() {
|
|
const qb = {
|
|
leftJoinAndSelect: jest.fn(),
|
|
where: jest.fn(),
|
|
orderBy: jest.fn(),
|
|
andWhere: jest.fn(),
|
|
getMany: jest.fn().mockResolvedValue([]),
|
|
};
|
|
qb.leftJoinAndSelect.mockReturnValue(qb);
|
|
qb.where.mockReturnValue(qb);
|
|
qb.orderBy.mockReturnValue(qb);
|
|
qb.andWhere.mockReturnValue(qb);
|
|
return qb;
|
|
}
|
|
|
|
describe('batch restore service semantics', () => {
|
|
it('rejects empty and invalid ids in every restore service', async () => {
|
|
const students = new StudentsService(
|
|
{} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
|
|
{} as never, {} as never, {} as never, {} as never, {} as never,
|
|
);
|
|
const rooms = new RoomsService(
|
|
{} as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
|
|
);
|
|
const expenses = new ExpensesService(
|
|
{} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
|
|
);
|
|
const occupancies = new OccupanciesService(
|
|
{} as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
|
|
);
|
|
const calls = [
|
|
(ids: number[]) => students.batchRestore(ids),
|
|
(ids: number[]) => rooms.batchRestore(ids),
|
|
(ids: number[]) => expenses.batchRestoreRoomExpenses(ids),
|
|
(ids: number[]) => expenses.batchRestorePersonalExpenses(ids),
|
|
(ids: number[]) => occupancies.batchRestore(ids),
|
|
];
|
|
for (const call of calls) {
|
|
await expect(call([])).rejects.toBeInstanceOf(BadRequestException);
|
|
await expect(call([0])).rejects.toBeInstanceOf(BadRequestException);
|
|
await expect(call([1.5])).rejects.toBeInstanceOf(BadRequestException);
|
|
}
|
|
});
|
|
|
|
it('deduplicates student ids, restores archived rows, and skips active rows', async () => {
|
|
const qb = updateQb();
|
|
const repo = {
|
|
find: jest.fn().mockResolvedValue([
|
|
{ id: 1, status: 'archived' },
|
|
{ id: 2, status: 'active' },
|
|
]),
|
|
createQueryBuilder: jest.fn(() => qb),
|
|
};
|
|
const service = new StudentsService(
|
|
repo as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
);
|
|
|
|
await expect(service.batchRestore([1, 1, 2])).resolves.toEqual({
|
|
message: '已批量恢复 1 名学生',
|
|
restored: 1,
|
|
skipped: 1,
|
|
});
|
|
expect(repo.find).toHaveBeenCalledWith({ where: { id: expect.anything() } });
|
|
expect(qb.set).toHaveBeenCalledWith({ status: 'active' });
|
|
expect(qb.where).toHaveBeenCalledWith('id IN (:...ids)', { ids: [1] });
|
|
});
|
|
|
|
it('rejects missing student ids before updating', async () => {
|
|
const repo = { find: jest.fn().mockResolvedValue([{ id: 1, status: 'archived' }]), createQueryBuilder: jest.fn() };
|
|
const service = new StudentsService(
|
|
repo as never, {} as never, {} as never, {} as never, {} as never, {} as never,
|
|
{} as never, {} as never, {} as never, {} as never, {} as never,
|
|
);
|
|
await expect(service.batchRestore([1, 2])).rejects.toBeInstanceOf(NotFoundException);
|
|
expect(repo.createQueryBuilder).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('restores archived rooms to available and skips non-archived rooms', async () => {
|
|
const qb = updateQb();
|
|
const repo = {
|
|
find: jest.fn().mockResolvedValue([
|
|
{ id: 1, status: 'archived' },
|
|
{ id: 2, status: 'maintenance' },
|
|
]),
|
|
createQueryBuilder: jest.fn(() => qb),
|
|
};
|
|
const service = new RoomsService(
|
|
repo as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
|
|
);
|
|
await expect(service.batchRestore([1, 2])).resolves.toEqual({
|
|
message: '已批量恢复 1 间宿舍', restored: 1, skipped: 1,
|
|
});
|
|
expect(qb.set).toHaveBeenCalledWith({ status: 'available' });
|
|
});
|
|
|
|
it('rejects room-expense restore when any selected record is billed', async () => {
|
|
const qb = updateQb();
|
|
const roomExpRepo = {
|
|
find: jest.fn().mockResolvedValue([{ id: 1, status: 'archived' }]),
|
|
createQueryBuilder: jest.fn(() => qb),
|
|
};
|
|
const billItemsRepo = { count: jest.fn().mockResolvedValue(1) };
|
|
const service = new ExpensesService(
|
|
roomExpRepo as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{ getRepository: jest.fn(() => billItemsRepo) } as never,
|
|
);
|
|
await expect(service.batchRestoreRoomExpenses([1])).rejects.toBeInstanceOf(BadRequestException);
|
|
expect(roomExpRepo.createQueryBuilder).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('restores unbilled room expenses and reports active rows as skipped', async () => {
|
|
const qb = updateQb();
|
|
const roomExpRepo = {
|
|
find: jest.fn().mockResolvedValue([
|
|
{ id: 1, status: 'archived' },
|
|
{ id: 2, status: 'active' },
|
|
]),
|
|
createQueryBuilder: jest.fn(() => qb),
|
|
};
|
|
const service = new ExpensesService(
|
|
roomExpRepo as never, {} as never, {} as never, {} as never, {} as never,
|
|
{ getRepository: jest.fn(() => ({ count: jest.fn().mockResolvedValue(0) })) } as never,
|
|
);
|
|
await expect(service.batchRestoreRoomExpenses([1, 2])).resolves.toMatchObject({ restored: 1, skipped: 1 });
|
|
expect(qb.set).toHaveBeenCalledWith({ status: 'active' });
|
|
});
|
|
|
|
it('skips an active billed room expense without blocking an archived unbilled expense', async () => {
|
|
const qb = updateQb();
|
|
const count = jest.fn().mockResolvedValue(0);
|
|
const roomExpRepo = {
|
|
find: jest.fn().mockResolvedValue([
|
|
{ id: 1, status: 'archived' },
|
|
{ id: 2, status: 'active' },
|
|
]),
|
|
createQueryBuilder: jest.fn(() => qb),
|
|
};
|
|
const service = new ExpensesService(
|
|
roomExpRepo as never, {} as never, {} as never, {} as never, {} as never,
|
|
{ getRepository: jest.fn(() => ({ count })) } as never,
|
|
);
|
|
await expect(service.batchRestoreRoomExpenses([1, 2])).resolves.toMatchObject({
|
|
restored: 1,
|
|
skipped: 1,
|
|
});
|
|
expect(count).toHaveBeenCalledWith({ where: { roomExpenseId: expect.anything() } });
|
|
expect(qb.where).toHaveBeenCalledWith('id IN (:...ids)', { ids: [1] });
|
|
});
|
|
|
|
it('rejects personal-expense restore when any selected record has a bill id', async () => {
|
|
const personalExpRepo = {
|
|
find: jest.fn().mockResolvedValue([{ id: 1, status: 'archived', billId: 9 }]),
|
|
createQueryBuilder: jest.fn(),
|
|
};
|
|
const service = new ExpensesService(
|
|
{} as never, personalExpRepo as never, {} as never, {} as never, {} as never, {} as never,
|
|
);
|
|
await expect(service.batchRestorePersonalExpenses([1])).rejects.toBeInstanceOf(BadRequestException);
|
|
expect(personalExpRepo.createQueryBuilder).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('restores unbilled personal expenses and skips already active records', async () => {
|
|
const qb = updateQb();
|
|
const personalExpRepo = {
|
|
find: jest.fn().mockResolvedValue([
|
|
{ id: 1, status: 'archived', billId: null },
|
|
{ id: 2, status: 'active', billId: null },
|
|
]),
|
|
createQueryBuilder: jest.fn(() => qb),
|
|
};
|
|
const service = new ExpensesService(
|
|
{} as never, personalExpRepo as never, {} as never, {} as never, {} as never, {} as never,
|
|
);
|
|
await expect(service.batchRestorePersonalExpenses([1, 1, 2])).resolves.toMatchObject({
|
|
restored: 1,
|
|
skipped: 1,
|
|
});
|
|
expect(qb.where).toHaveBeenCalledWith('id IN (:...ids)', { ids: [1] });
|
|
});
|
|
|
|
it('skips an active billed personal expense without blocking an archived unbilled expense', async () => {
|
|
const qb = updateQb();
|
|
const personalExpRepo = {
|
|
find: jest.fn().mockResolvedValue([
|
|
{ id: 1, status: 'archived', billId: null },
|
|
{ id: 2, status: 'active', billId: 9 },
|
|
]),
|
|
createQueryBuilder: jest.fn(() => qb),
|
|
};
|
|
const service = new ExpensesService(
|
|
{} as never, personalExpRepo as never, {} as never, {} as never, {} as never, {} as never,
|
|
);
|
|
await expect(service.batchRestorePersonalExpenses([1, 2])).resolves.toMatchObject({
|
|
restored: 1,
|
|
skipped: 1,
|
|
});
|
|
expect(qb.where).toHaveBeenCalledWith('id IN (:...ids)', { ids: [1] });
|
|
});
|
|
|
|
it('uses archived status when querying expense archive views', async () => {
|
|
const roomQb = listQb();
|
|
const personalRepo = { find: jest.fn().mockResolvedValue([]) };
|
|
const service = new ExpensesService(
|
|
{ createQueryBuilder: jest.fn(() => roomQb) } as never,
|
|
personalRepo as never,
|
|
{} as never, {} as never, {} as never, {} as never,
|
|
);
|
|
await service.findRoomExpenses({ status: 'archived' });
|
|
await service.findPersonalExpenses({ status: 'archived' });
|
|
expect(roomQb.where).toHaveBeenCalledWith('e.status = :status', { status: 'archived' });
|
|
expect(personalRepo.find).toHaveBeenCalledWith(expect.objectContaining({ where: { status: 'archived' } }));
|
|
});
|
|
|
|
it('rejects invalid expense query status values', async () => {
|
|
const service = new ExpensesService(
|
|
{} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
|
|
);
|
|
await expect(service.findRoomExpenses({ status: 'deleted' as never })).rejects.toBeInstanceOf(BadRequestException);
|
|
await expect(service.findPersonalExpenses({ status: 'deleted' as never })).rejects.toBeInstanceOf(BadRequestException);
|
|
});
|
|
|
|
it('rejects an archived occupancy without a checkout date before updating', async () => {
|
|
const repo = {
|
|
find: jest.fn().mockResolvedValue([{ id: 1, status: 'archived', checkOutDate: null }]),
|
|
createQueryBuilder: jest.fn(),
|
|
};
|
|
const service = new OccupanciesService(
|
|
repo as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
|
|
);
|
|
await expect(service.batchRestore([1])).rejects.toBeInstanceOf(BadRequestException);
|
|
expect(repo.createQueryBuilder).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('restores checked-out occupancies without changing room, bed, or locker state', async () => {
|
|
const qb = updateQb();
|
|
const repo = {
|
|
find: jest.fn().mockResolvedValue([
|
|
{ id: 1, status: 'archived', checkOutDate: '2026-07-01' },
|
|
{ id: 2, status: 'active', checkOutDate: '2026-07-02' },
|
|
]),
|
|
createQueryBuilder: jest.fn(() => qb),
|
|
};
|
|
const roomRepo = { update: jest.fn() };
|
|
const bedRepo = { update: jest.fn() };
|
|
const lockerRepo = { update: jest.fn() };
|
|
const service = new OccupanciesService(
|
|
repo as never, roomRepo as never, {} as never, {} as never, bedRepo as never,
|
|
lockerRepo as never, {} as never, {} as never,
|
|
);
|
|
await expect(service.batchRestore([1, 2])).resolves.toMatchObject({ restored: 1, skipped: 1 });
|
|
expect(qb.set).toHaveBeenCalledWith({ status: 'active' });
|
|
expect(roomRepo.update).not.toHaveBeenCalled();
|
|
expect(bedRepo.update).not.toHaveBeenCalled();
|
|
expect(lockerRepo.update).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('uses archived status while preserving active=true as checkout filtering', async () => {
|
|
const qb = listQb();
|
|
const service = new OccupanciesService(
|
|
{ createQueryBuilder: jest.fn(() => qb) } as never,
|
|
{} as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
|
|
);
|
|
await service.findAll({ status: 'archived', active: true });
|
|
expect(qb.where).toHaveBeenCalledWith('o.status = :status', { status: 'archived' });
|
|
expect(qb.andWhere).toHaveBeenCalledWith('o.checkOutDate IS NULL');
|
|
});
|
|
|
|
it('rejects invalid occupancy query status values', async () => {
|
|
const service = new OccupanciesService(
|
|
{} as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
|
|
);
|
|
await expect(service.findAll({ status: 'deleted' as never })).rejects.toBeInstanceOf(BadRequestException);
|
|
});
|
|
});
|