import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { NotFoundException, BadRequestException, ForbiddenException, } from '@nestjs/common'; import { LedgersService } from './ledgers.service'; import { Ledger } from '../../entities/ledger.entity'; import { Group } from '../../entities/group.entity'; import { GroupMember } from '../../entities/group-member.entity'; enum LedgerType { INCOME = 'income', EXPENSE = 'expense', } describe('LedgersService', () => { let service: LedgersService; let mockLedgerRepository: any; let mockGroupRepository: any; let mockGroupMemberRepository: any; const mockUser = { id: 'user-1', username: 'testuser' }; const mockGroup = { id: 'group-1', name: '测试小组', isActive: true, parentId: null, }; const mockMembership = { id: 'member-1', userId: 'user-1', groupId: 'group-1', role: 'member', isActive: true, }; const mockLedger = { id: 'ledger-1', groupId: 'group-1', creatorId: 'user-1', type: LedgerType.INCOME, amount: 100, category: '聚餐费用', description: '周末聚餐', createdAt: new Date('2024-01-20T10:00:00Z'), updatedAt: new Date(), }; beforeEach(async () => { mockLedgerRepository = { create: jest.fn(), save: jest.fn(), findOne: jest.fn(), remove: jest.fn(), createQueryBuilder: jest.fn(), }; mockGroupRepository = { findOne: jest.fn(), find: jest.fn(), }; mockGroupMemberRepository = { findOne: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ providers: [ LedgersService, { provide: getRepositoryToken(Ledger), useValue: mockLedgerRepository, }, { provide: getRepositoryToken(Group), useValue: mockGroupRepository, }, { provide: getRepositoryToken(GroupMember), useValue: mockGroupMemberRepository, }, ], }).compile(); service = module.get(LedgersService); }); describe('create', () => { it('应该成功创建账目', async () => { mockGroupRepository.findOne.mockResolvedValue(mockGroup); mockGroupMemberRepository.findOne.mockResolvedValue(mockMembership); mockLedgerRepository.create.mockReturnValue(mockLedger); mockLedgerRepository.save.mockResolvedValue(mockLedger); mockLedgerRepository.findOne.mockResolvedValue({ ...mockLedger, group: mockGroup, creator: mockUser, }); const result = await service.create('user-1', { groupId: 'group-1', type: LedgerType.INCOME, amount: 100, category: '聚餐费用', description: '周末聚餐', }); expect(result).toHaveProperty('id'); expect(result.amount).toBe(100); expect(mockLedgerRepository.save).toHaveBeenCalled(); }); it('应该在小组不存在时抛出异常', async () => { mockGroupRepository.findOne.mockResolvedValue(null); await expect( service.create('user-1', { groupId: 'group-1', type: LedgerType.INCOME, amount: 100, category: '聚餐费用', description: '测试', }), ).rejects.toThrow(NotFoundException); }); it('应该在用户不在小组中时抛出异常', async () => { mockGroupRepository.findOne.mockResolvedValue(mockGroup); mockGroupMemberRepository.findOne.mockResolvedValue(null); await expect( service.create('user-1', { groupId: 'group-1', type: LedgerType.INCOME, amount: 100, category: '聚餐费用', description: '测试', }), ).rejects.toThrow(ForbiddenException); }); it('应该在金额无效时抛出异常', async () => { mockGroupRepository.findOne.mockResolvedValue(mockGroup); mockGroupMemberRepository.findOne.mockResolvedValue(mockMembership); await expect( service.create('user-1', { groupId: 'group-1', type: LedgerType.INCOME, amount: -100, category: '聚餐费用', description: '测试', }), ).rejects.toThrow(BadRequestException); }); }); describe('findAll', () => { it('应该成功获取账目列表', async () => { const mockQueryBuilder = { leftJoinAndSelect: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), andWhere: jest.fn().mockReturnThis(), orderBy: jest.fn().mockReturnThis(), skip: jest.fn().mockReturnThis(), take: jest.fn().mockReturnThis(), getManyAndCount: jest.fn().mockResolvedValue([[mockLedger], 1]), }; mockLedgerRepository.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); }); it('应该支持按类型筛选', async () => { const mockQueryBuilder = { leftJoinAndSelect: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), andWhere: jest.fn().mockReturnThis(), orderBy: jest.fn().mockReturnThis(), skip: jest.fn().mockReturnThis(), take: jest.fn().mockReturnThis(), getManyAndCount: jest.fn().mockResolvedValue([[mockLedger], 1]), }; mockLedgerRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); mockGroupMemberRepository.findOne.mockResolvedValue(mockMembership); const result = await service.findAll('user-1', { groupId: 'group-1', type: LedgerType.INCOME, page: 1, limit: 10, }); expect(result.items).toHaveLength(1); expect(mockQueryBuilder.andWhere).toHaveBeenCalled(); }); }); describe('findOne', () => { it('应该成功获取账目详情', async () => { mockLedgerRepository.findOne.mockResolvedValue({ ...mockLedger, group: mockGroup, creator: mockUser, }); const result = await service.findOne('ledger-1'); expect(result).toHaveProperty('id'); expect(result.id).toBe('ledger-1'); }); it('应该在账目不存在时抛出异常', async () => { mockLedgerRepository.findOne.mockResolvedValue(null); await expect(service.findOne('ledger-1')).rejects.toThrow( NotFoundException, ); }); }); describe('update', () => { it('应该成功更新账目', async () => { mockLedgerRepository.findOne .mockResolvedValueOnce(mockLedger) .mockResolvedValueOnce({ ...mockLedger, amount: 200, group: mockGroup, creator: mockUser, }); mockGroupMemberRepository.findOne.mockResolvedValue({ ...mockMembership, role: 'admin', }); mockLedgerRepository.save.mockResolvedValue({ ...mockLedger, amount: 200, }); const result = await service.update('user-1', 'ledger-1', { amount: 200, }); expect(result.amount).toBe(200); }); it('应该在账目不存在时抛出异常', async () => { mockLedgerRepository.findOne.mockResolvedValue(null); await expect( service.update('user-1', 'ledger-1', { amount: 200 }), ).rejects.toThrow(NotFoundException); }); it('应该在无权限时抛出异常', async () => { mockLedgerRepository.findOne.mockResolvedValue(mockLedger); mockGroupMemberRepository.findOne.mockResolvedValue({ ...mockMembership, role: 'member', }); await expect( service.update('user-2', 'ledger-1', { amount: 200 }), ).rejects.toThrow(ForbiddenException); }); }); describe('remove', () => { it('应该成功删除账目', async () => { mockLedgerRepository.findOne.mockResolvedValue(mockLedger); mockGroupMemberRepository.findOne.mockResolvedValue({ ...mockMembership, role: 'admin', }); mockLedgerRepository.remove.mockResolvedValue(mockLedger); const result = await service.remove('user-1', 'ledger-1'); expect(result).toHaveProperty('message'); }); it('应该在无权限时抛出异常', async () => { mockLedgerRepository.findOne.mockResolvedValue(mockLedger); mockGroupMemberRepository.findOne.mockResolvedValue({ ...mockMembership, role: 'member', }); await expect( service.remove('user-2', 'ledger-1'), ).rejects.toThrow(ForbiddenException); }); }); describe('getMonthlyStatistics', () => { it('应该成功获取月度统计', async () => { const mockQueryBuilder = { where: jest.fn().mockReturnThis(), andWhere: jest.fn().mockReturnThis(), getMany: jest.fn().mockResolvedValue([ { ...mockLedger, type: LedgerType.INCOME, amount: 100 }, { ...mockLedger, type: LedgerType.EXPENSE, amount: 50 }, ]), }; mockLedgerRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); mockGroupMemberRepository.findOne.mockResolvedValue(mockMembership); const result = await service.getMonthlyStatistics('user-1', { groupId: 'group-1', year: 2024, month: 1, }); expect(result).toHaveProperty('income'); expect(result).toHaveProperty('expense'); expect(result).toHaveProperty('balance'); expect(result).toHaveProperty('categories'); }); it('应该在用户不在小组时抛出异常', async () => { mockGroupMemberRepository.findOne.mockResolvedValue(null); await expect( service.getMonthlyStatistics('user-1', { groupId: 'group-1', year: 2024, month: 1, }), ).rejects.toThrow(ForbiddenException); }); }); describe('getHierarchicalSummary', () => { it('应该成功获取层级汇总', async () => { const childGroup = { id: 'group-2', name: '子小组', parentId: 'group-1' }; const mockQueryBuilder = { where: jest.fn().mockReturnThis(), getMany: jest.fn().mockResolvedValue([mockLedger]), }; mockGroupRepository.findOne.mockResolvedValue(mockGroup); mockGroupMemberRepository.findOne.mockResolvedValue(mockMembership); mockGroupRepository.find.mockResolvedValue([childGroup]); mockLedgerRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); const result = await service.getHierarchicalSummary('user-1', 'group-1'); expect(result).toHaveProperty('groupId'); expect(result).toHaveProperty('income'); expect(result).toHaveProperty('expense'); expect(result).toHaveProperty('balance'); }); }); });