import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { NotFoundException, ForbiddenException, BadRequestException, } from '@nestjs/common'; import { SchedulesService } from './schedules.service'; import { Schedule } from '../../entities/schedule.entity'; import { Group } from '../../entities/group.entity'; import { GroupMember } from '../../entities/group-member.entity'; import { TimeSlotDto } from './dto/schedule.dto'; describe('SchedulesService', () => { let service: SchedulesService; let mockScheduleRepository: any; let mockGroupRepository: any; let mockGroupMemberRepository: any; const mockUser = { id: 'user-1', username: 'testuser' }; const mockGroup = { id: 'group-1', name: '测试小组', isActive: true }; const mockMembership = { id: 'member-1', userId: 'user-1', groupId: 'group-1', role: 'member', isActive: true, }; const mockTimeSlots: TimeSlotDto[] = [ { startTime: new Date('2024-01-20T19:00:00Z'), endTime: new Date('2024-01-20T21:00:00Z'), note: '晚上空闲', }, { startTime: new Date('2024-01-21T14:00:00Z'), endTime: new Date('2024-01-21T17:00:00Z'), note: '下午空闲', }, ]; const mockSchedule = { id: 'schedule-1', userId: 'user-1', groupId: 'group-1', availableSlots: mockTimeSlots, createdAt: new Date(), updatedAt: new Date(), }; beforeEach(async () => { mockScheduleRepository = { create: jest.fn(), save: jest.fn(), find: jest.fn(), findOne: jest.fn(), remove: jest.fn(), createQueryBuilder: jest.fn(), }; mockGroupRepository = { findOne: jest.fn(), }; mockGroupMemberRepository = { find: jest.fn(), findOne: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ providers: [ SchedulesService, { provide: getRepositoryToken(Schedule), useValue: mockScheduleRepository, }, { provide: getRepositoryToken(Group), useValue: mockGroupRepository, }, { provide: getRepositoryToken(GroupMember), useValue: mockGroupMemberRepository, }, ], }).compile(); service = module.get(SchedulesService); }); describe('create', () => { it('应该成功创建排班', async () => { mockGroupRepository.findOne.mockResolvedValue(mockGroup); mockGroupMemberRepository.findOne.mockResolvedValue(mockMembership); mockScheduleRepository.create.mockReturnValue(mockSchedule); mockScheduleRepository.save.mockResolvedValue(mockSchedule); mockScheduleRepository.findOne.mockResolvedValue({ ...mockSchedule, user: mockUser, group: mockGroup, }); const result = await service.create('user-1', { groupId: 'group-1', title: '测试排班', availableSlots: mockTimeSlots, }); expect(result).toHaveProperty('id'); expect(mockScheduleRepository.save).toHaveBeenCalled(); }); it('应该在小组不存在时抛出异常', async () => { mockGroupRepository.findOne.mockResolvedValue(null); await expect( service.create('user-1', { groupId: 'group-1', title: '测试排班', availableSlots: mockTimeSlots, }), ).rejects.toThrow(NotFoundException); }); it('应该在用户不在小组中时抛出异常', async () => { mockGroupRepository.findOne.mockResolvedValue(mockGroup); mockGroupMemberRepository.findOne.mockResolvedValue(null); await expect( service.create('user-1', { groupId: 'group-1', title: '测试排班', availableSlots: mockTimeSlots, }), ).rejects.toThrow(ForbiddenException); }); it('应该在时间段为空时抛出异常', async () => { mockGroupRepository.findOne.mockResolvedValue(mockGroup); mockGroupMemberRepository.findOne.mockResolvedValue(mockMembership); await expect( service.create('user-1', { groupId: 'group-1', title: '测试排班', availableSlots: [], }), ).rejects.toThrow(BadRequestException); }); it('应该在时间段无效时抛出异常', async () => { mockGroupRepository.findOne.mockResolvedValue(mockGroup); mockGroupMemberRepository.findOne.mockResolvedValue(mockMembership); await expect( service.create('user-1', { groupId: 'group-1', title: '测试排班', availableSlots: [ { startTime: new Date('2024-01-20T21:00:00Z'), endTime: new Date('2024-01-20T19:00:00Z'), // 结束时间早于开始时间 }, ], }), ).rejects.toThrow(BadRequestException); }); }); describe('findAll', () => { it('应该成功获取排班列表', async () => { const mockQueryBuilder = { leftJoinAndSelect: jest.fn().mockReturnThis(), andWhere: jest.fn().mockReturnThis(), orderBy: jest.fn().mockReturnThis(), skip: jest.fn().mockReturnThis(), take: jest.fn().mockReturnThis(), getManyAndCount: jest .fn() .mockResolvedValue([[mockSchedule], 1]), }; mockScheduleRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); mockGroupMemberRepository.findOne.mockResolvedValue(mockMembership); const result = await service.findAll('user-1', { groupId: 'group-1', page: 1, limit: 10, }); expect(result).toHaveProperty('items'); expect(result).toHaveProperty('total'); expect(result.items).toHaveLength(1); expect(result.total).toBe(1); }); it('应该在指定小组且用户不在小组时抛出异常', async () => { const mockQueryBuilder = { leftJoinAndSelect: jest.fn().mockReturnThis(), andWhere: jest.fn().mockReturnThis(), orderBy: jest.fn().mockReturnThis(), skip: jest.fn().mockReturnThis(), take: jest.fn().mockReturnThis(), getManyAndCount: jest .fn() .mockResolvedValue([[mockSchedule], 1]), }; mockScheduleRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); mockGroupMemberRepository.findOne.mockResolvedValue(null); await expect( service.findAll('user-1', { groupId: 'group-1', }), ).rejects.toThrow(ForbiddenException); }); it('应该在无小组ID时返回用户所在所有小组的排班', async () => { const mockQueryBuilder = { leftJoinAndSelect: jest.fn().mockReturnThis(), andWhere: jest.fn().mockReturnThis(), orderBy: jest.fn().mockReturnThis(), skip: jest.fn().mockReturnThis(), take: jest.fn().mockReturnThis(), getManyAndCount: jest .fn() .mockResolvedValue([[mockSchedule], 1]), }; mockScheduleRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); mockGroupMemberRepository.find.mockResolvedValue([ { groupId: 'group-1' }, { groupId: 'group-2' }, ]); const result = await service.findAll('user-1', {}); expect(result.items).toHaveLength(1); expect(mockGroupMemberRepository.find).toHaveBeenCalled(); }); }); describe('findOne', () => { it('应该成功获取排班详情', async () => { mockScheduleRepository.findOne.mockResolvedValue({ ...mockSchedule, user: mockUser, group: mockGroup, }); const result = await service.findOne('schedule-1'); expect(result).toHaveProperty('id'); expect(result.id).toBe('schedule-1'); }); it('应该在排班不存在时抛出异常', async () => { mockScheduleRepository.findOne.mockResolvedValue(null); await expect(service.findOne('schedule-1')).rejects.toThrow( NotFoundException, ); }); }); describe('update', () => { it('应该成功更新排班', async () => { mockScheduleRepository.findOne .mockResolvedValueOnce(mockSchedule) .mockResolvedValueOnce({ ...mockSchedule, user: mockUser, group: mockGroup, }); mockScheduleRepository.save.mockResolvedValue(mockSchedule); const result = await service.update('user-1', 'schedule-1', { availableSlots: mockTimeSlots, }); expect(result).toHaveProperty('id'); expect(mockScheduleRepository.save).toHaveBeenCalled(); }); it('应该在排班不存在时抛出异常', async () => { mockScheduleRepository.findOne.mockResolvedValue(null); await expect( service.update('user-1', 'schedule-1', { availableSlots: mockTimeSlots }), ).rejects.toThrow(NotFoundException); }); it('应该在非创建者更新时抛出异常', async () => { mockScheduleRepository.findOne.mockResolvedValue(mockSchedule); await expect( service.update('user-2', 'schedule-1', { availableSlots: mockTimeSlots }), ).rejects.toThrow(ForbiddenException); }); }); describe('remove', () => { it('应该成功删除排班', async () => { mockScheduleRepository.findOne.mockResolvedValue(mockSchedule); mockScheduleRepository.remove.mockResolvedValue(mockSchedule); const result = await service.remove('user-1', 'schedule-1'); expect(result).toHaveProperty('message'); expect(mockScheduleRepository.remove).toHaveBeenCalled(); }); it('应该在排班不存在时抛出异常', async () => { mockScheduleRepository.findOne.mockResolvedValue(null); await expect(service.remove('user-1', 'schedule-1')).rejects.toThrow( NotFoundException, ); }); it('应该在非创建者删除时抛出异常', async () => { mockScheduleRepository.findOne.mockResolvedValue(mockSchedule); await expect(service.remove('user-2', 'schedule-1')).rejects.toThrow( ForbiddenException, ); }); }); describe('findCommonSlots', () => { it('应该成功查找共同空闲时间', async () => { mockGroupMemberRepository.findOne.mockResolvedValue(mockMembership); mockScheduleRepository.find.mockResolvedValue([ { ...mockSchedule, userId: 'user-1', user: { id: 'user-1' }, }, { ...mockSchedule, id: 'schedule-2', userId: 'user-2', user: { id: 'user-2' }, availableSlots: [ { startTime: new Date('2024-01-20T19:30:00Z'), endTime: new Date('2024-01-20T22:00:00Z'), }, ], }, ]); const result = await service.findCommonSlots('user-1', { groupId: 'group-1', startTime: new Date('2024-01-20T00:00:00Z'), endTime: new Date('2024-01-22T00:00:00Z'), minParticipants: 2, }); expect(result).toHaveProperty('commonSlots'); expect(result).toHaveProperty('totalParticipants'); expect(result.totalParticipants).toBe(2); }); it('应该在用户不在小组时抛出异常', async () => { mockGroupMemberRepository.findOne.mockResolvedValue(null); await expect( service.findCommonSlots('user-1', { groupId: 'group-1', startTime: new Date('2024-01-20T00:00:00Z'), endTime: new Date('2024-01-22T00:00:00Z'), }), ).rejects.toThrow(ForbiddenException); }); it('应该在没有排班数据时返回空结果', async () => { mockGroupMemberRepository.findOne.mockResolvedValue(mockMembership); mockScheduleRepository.find.mockResolvedValue([]); const result = await service.findCommonSlots('user-1', { groupId: 'group-1', startTime: new Date('2024-01-20T00:00:00Z'), endTime: new Date('2024-01-22T00:00:00Z'), }); expect(result.commonSlots).toEqual([]); expect(result.message).toBe('暂无排班数据'); }); }); });