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"); }); }); });