forked from wangziqi/gongxue-base
- Delete department.entity.ts, user-department.entity.ts - Remove Department/UserDepartment from entities/index.ts - Remove departmentId column from 18 entities (AttendanceRecord, ArchiveAttachment, Bill, ClassSchedule, Classroom, ClassroomRental, Deposit, DepositInstallment, ExamScore, LearningRecord, Occupancy, PersonalExpense, ResultArchive, Room, RoomExpense, Student, StudentEnrollment, StudentProfile, StudentReport) - Remove departments/ module entirely - Delete campus-scope.ts, campus-scope.middleware.ts (request-utils.ts kept — it's just IP extraction) - Simplify common.module.ts to empty module - Remove CampusScopeMiddleware from app.module.ts - Remove all CampusScope injections and filter calls across all services - Remove departmentId from all DTOs and controllers - Simplify dingtalk/wecom sync to only sync users (no dept table) - Update seed module to remove department seeding - Clean frontend compilation
174 lines
7.4 KiB
TypeScript
174 lines
7.4 KiB
TypeScript
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
|
import { ConflictException } from '@nestjs/common';
|
|
import { Repository } from 'typeorm';
|
|
import { SchedulesService } from './schedules.service';
|
|
import { ClassSchedule, ScheduleType } from '../entities/class-schedule.entity';
|
|
import { ClassroomRental } from '../entities/classroom-rental.entity';
|
|
import { Class } from '../entities/class.entity';
|
|
|
|
/** Build a mock query-builder where each chain method returns `this`. */
|
|
function mockQueryBuilder<T>(results: T[] = []) {
|
|
const qb = {
|
|
where: jest.fn().mockReturnThis(),
|
|
andWhere: jest.fn().mockReturnThis(),
|
|
orderBy: jest.fn().mockReturnThis(),
|
|
addOrderBy: jest.fn().mockReturnThis(),
|
|
getMany: jest.fn().mockResolvedValue(results),
|
|
};
|
|
return qb;
|
|
}
|
|
|
|
describe('SchedulesService — checkConflict', () => {
|
|
let service: SchedulesService;
|
|
let scheduleRepo: jest.Mocked<Pick<Repository<ClassSchedule>, 'createQueryBuilder'>>;
|
|
let rentalRepo: jest.Mocked<Pick<Repository<ClassroomRental>, 'createQueryBuilder'>>;
|
|
|
|
beforeEach(async () => {
|
|
const mockRepo = {
|
|
createQueryBuilder: jest.fn(),
|
|
};
|
|
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
SchedulesService,
|
|
{ provide: getRepositoryToken(ClassSchedule), useValue: mockRepo },
|
|
{ provide: getRepositoryToken(Class), useValue: { find: jest.fn().mockResolvedValue([]) } },
|
|
{ provide: getRepositoryToken(ClassroomRental), useValue: { createQueryBuilder: jest.fn() } },
|
|
],
|
|
}).compile();
|
|
|
|
service = module.get<SchedulesService>(SchedulesService);
|
|
scheduleRepo = module.get(getRepositoryToken(ClassSchedule));
|
|
rentalRepo = module.get(getRepositoryToken(ClassroomRental));
|
|
});
|
|
|
|
it('same classroom + same weekday + overlapping times → ConflictException', async () => {
|
|
const qb = mockQueryBuilder<ClassSchedule>([
|
|
{ id: 1, subject: '数学', startTime: '08:00', endTime: '10:00' } as ClassSchedule,
|
|
]);
|
|
const rentalQb = mockQueryBuilder<ClassroomRental>([]);
|
|
(scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
|
|
(rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);
|
|
|
|
await expect(
|
|
service.checkConflict(1, 3, '09:00', '11:00', '2026-03-01', '2026-06-30'),
|
|
).rejects.toThrow(ConflictException);
|
|
});
|
|
|
|
it('same classroom + same weekday + non-overlapping times → no conflict', async () => {
|
|
const qb = mockQueryBuilder<ClassSchedule>([]);
|
|
const rentalQb = mockQueryBuilder<ClassroomRental>([]);
|
|
(scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
|
|
(rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);
|
|
|
|
await expect(
|
|
service.checkConflict(1, 3, '10:00', '12:00', '2026-03-01', '2026-06-30'),
|
|
).resolves.toEqual([]);
|
|
});
|
|
|
|
it('same classroom + same weekday + overlapping times but disjoint date ranges → no conflict', async () => {
|
|
const qb = mockQueryBuilder<ClassSchedule>([]);
|
|
const rentalQb = mockQueryBuilder<ClassroomRental>([]);
|
|
(scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
|
|
(rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);
|
|
|
|
await expect(
|
|
service.checkConflict(1, 3, '09:00', '11:00', '2026-07-01', '2026-08-31'),
|
|
).resolves.toEqual([]);
|
|
});
|
|
|
|
it('different classroom → no conflict', async () => {
|
|
const qb = mockQueryBuilder<ClassSchedule>([]);
|
|
const rentalQb = mockQueryBuilder<ClassroomRental>([]);
|
|
(scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
|
|
(rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);
|
|
|
|
await expect(
|
|
service.checkConflict(2, 3, '09:00', '11:00', '2026-03-01', '2026-06-30'),
|
|
).resolves.toEqual([]);
|
|
});
|
|
|
|
it('excludes the given schedule id from conflict check', async () => {
|
|
const qb = mockQueryBuilder<ClassSchedule>([]);
|
|
const rentalQb = mockQueryBuilder<ClassroomRental>([]);
|
|
(scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
|
|
(rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);
|
|
|
|
await service.checkConflict(1, 3, '09:00', '11:00', '2026-03-01', '2026-06-30', 42);
|
|
|
|
expect(qb.andWhere).toHaveBeenCalledWith('cs.id != :excludeId', { excludeId: 42 });
|
|
});
|
|
|
|
it('overlapping classroom rental → ConflictException', async () => {
|
|
const qb = mockQueryBuilder<ClassSchedule>([]);
|
|
const rentalQb = mockQueryBuilder<ClassroomRental>([
|
|
{ id: 10, startDate: '2026-03-01', endDate: '2026-03-31', status: 'active' } as ClassroomRental,
|
|
]);
|
|
(scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
|
|
(rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);
|
|
|
|
await expect(
|
|
service.checkConflict(1, 3, '09:00', '11:00', '2026-03-01', '2026-06-30'),
|
|
).rejects.toThrow(ConflictException);
|
|
});
|
|
|
|
it('cancelled rental does not conflict', async () => {
|
|
const qb = mockQueryBuilder<ClassSchedule>([]);
|
|
const rentalQb = mockQueryBuilder<ClassroomRental>([]);
|
|
(scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
|
|
(rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);
|
|
|
|
await expect(
|
|
service.checkConflict(1, 3, '09:00', '11:00', '2026-03-01', '2026-06-30'),
|
|
).resolves.toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('SchedulesService — getClassroomOccupancy', () => {
|
|
let service: SchedulesService;
|
|
let scheduleRepo: jest.Mocked<Pick<Repository<ClassSchedule>, 'createQueryBuilder'>>;
|
|
|
|
beforeEach(async () => {
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
SchedulesService,
|
|
{ provide: getRepositoryToken(ClassSchedule), useValue: { createQueryBuilder: jest.fn() } },
|
|
{ provide: getRepositoryToken(Class), useValue: { find: jest.fn().mockResolvedValue([]) } },
|
|
{ provide: getRepositoryToken(ClassroomRental), useValue: { createQueryBuilder: jest.fn() } },
|
|
],
|
|
}).compile();
|
|
|
|
service = module.get<SchedulesService>(SchedulesService);
|
|
scheduleRepo = module.get(getRepositoryToken(ClassSchedule));
|
|
});
|
|
|
|
it('returns both INTERNAL and RENTAL schedules', async () => {
|
|
const internalSchedule = {
|
|
id: 1,
|
|
scheduleType: ScheduleType.INTERNAL,
|
|
subject: '数学',
|
|
} as ClassSchedule;
|
|
const rentalSchedule = {
|
|
id: 2,
|
|
scheduleType: ScheduleType.RENTAL,
|
|
subject: 'Tenant A 租赁',
|
|
} as ClassSchedule;
|
|
const qb = mockQueryBuilder<ClassSchedule>([internalSchedule, rentalSchedule]);
|
|
(scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
|
|
|
|
const result = await service.getClassroomOccupancy(1, '2026-03-15');
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result.map((s) => s.scheduleType)).toContain(ScheduleType.INTERNAL);
|
|
expect(result.map((s) => s.scheduleType)).toContain(ScheduleType.RENTAL);
|
|
expect(qb.where).toHaveBeenCalledWith('cs.classroomId = :classroomId', { classroomId: 1 });
|
|
expect(qb.andWhere).toHaveBeenCalledWith('cs.status = :status', { status: 'active' });
|
|
expect(qb.andWhere).toHaveBeenCalledWith('cs.scheduleType IN (:...scheduleTypes)', {
|
|
scheduleTypes: [ScheduleType.INTERNAL, ScheduleType.RENTAL],
|
|
});
|
|
expect(qb.andWhere).toHaveBeenCalledWith('cs.startDate <= :date', { date: '2026-03-15' });
|
|
expect(qb.andWhere).toHaveBeenCalledWith('cs.endDate >= :date', { date: '2026-03-15' });
|
|
});
|
|
});
|