import { describe, it, expect, beforeEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { ProjectsService } from "./projects.service"; import { PrismaService } from "../prisma/prisma.service"; import { ActivityService } from "../activity/activity.service"; import { WebSocketGateway } from "../websocket/websocket.gateway"; import { ProjectStatus, Prisma } from "@prisma/client"; import { NotFoundException } from "@nestjs/common"; describe("ProjectsService", () => { let service: ProjectsService; let prisma: PrismaService; let activityService: ActivityService; let wsGateway: WebSocketGateway; const mockPrismaService = { project: { create: vi.fn(), findMany: vi.fn(), count: vi.fn(), findUnique: vi.fn(), update: vi.fn(), delete: vi.fn(), }, }; const mockActivityService = { logProjectCreated: vi.fn(), logProjectUpdated: vi.fn(), logProjectDeleted: vi.fn(), }; const mockWebSocketGateway = { emitProjectUpdated: vi.fn(), }; const mockWorkspaceId = "550e8400-e29b-41d4-a716-446655440001"; const mockUserId = "550e8400-e29b-41d4-a716-446655440002"; const mockProjectId = "550e8400-e29b-41d4-a716-446655440003"; const mockProject = { id: mockProjectId, workspaceId: mockWorkspaceId, name: "Test Project", description: "Test Description", status: ProjectStatus.PLANNING, startDate: new Date("2026-02-01"), endDate: new Date("2026-03-01"), creatorId: mockUserId, color: "#FF5733", metadata: {}, createdAt: new Date(), updatedAt: new Date(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ ProjectsService, { provide: PrismaService, useValue: mockPrismaService, }, { provide: ActivityService, useValue: mockActivityService, }, { provide: WebSocketGateway, useValue: mockWebSocketGateway, }, ], }).compile(); service = module.get(ProjectsService); prisma = module.get(PrismaService); activityService = module.get(ActivityService); wsGateway = module.get(WebSocketGateway); vi.clearAllMocks(); }); it("should be defined", () => { expect(service).toBeDefined(); }); describe("create", () => { it("should create a project and log activity", async () => { const createDto = { name: "New Project", description: "Project description", color: "#FF5733", }; mockPrismaService.project.create.mockResolvedValue(mockProject); mockActivityService.logProjectCreated.mockResolvedValue({}); const result = await service.create(mockWorkspaceId, mockUserId, createDto); expect(result).toEqual(mockProject); expect(prisma.project.create).toHaveBeenCalledWith({ data: { ...createDto, workspaceId: mockWorkspaceId, creatorId: mockUserId, status: ProjectStatus.PLANNING, metadata: {}, }, include: { creator: { select: { id: true, name: true, email: true }, }, _count: { select: { tasks: true, events: true }, }, }, }); expect(activityService.logProjectCreated).toHaveBeenCalledWith( mockWorkspaceId, mockUserId, mockProject.id, { name: mockProject.name } ); }); }); describe("findAll", () => { it("should return paginated projects with default pagination", async () => { const projects = [mockProject]; mockPrismaService.project.findMany.mockResolvedValue(projects); mockPrismaService.project.count.mockResolvedValue(1); const result = await service.findAll({ workspaceId: mockWorkspaceId }); expect(result).toEqual({ data: projects, meta: { total: 1, page: 1, limit: 50, totalPages: 1, }, }); }); it("should filter by status", async () => { mockPrismaService.project.findMany.mockResolvedValue([mockProject]); mockPrismaService.project.count.mockResolvedValue(1); await service.findAll({ workspaceId: mockWorkspaceId, status: ProjectStatus.ACTIVE, }); expect(prisma.project.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: { workspaceId: mockWorkspaceId, status: ProjectStatus.ACTIVE, }, }) ); }); }); describe("findOne", () => { it("should return a project by id", async () => { mockPrismaService.project.findUnique.mockResolvedValue(mockProject); const result = await service.findOne(mockProjectId, mockWorkspaceId); expect(result).toEqual(mockProject); }); it("should throw NotFoundException if project not found", async () => { mockPrismaService.project.findUnique.mockResolvedValue(null); await expect( service.findOne(mockProjectId, mockWorkspaceId) ).rejects.toThrow(NotFoundException); }); it("should enforce workspace isolation when finding project", async () => { const otherWorkspaceId = "550e8400-e29b-41d4-a716-446655440099"; mockPrismaService.project.findUnique.mockResolvedValue(null); await expect(service.findOne(mockProjectId, otherWorkspaceId)).rejects.toThrow( NotFoundException ); expect(prisma.project.findUnique).toHaveBeenCalledWith({ where: { id: mockProjectId, workspaceId: otherWorkspaceId, }, include: expect.any(Object), }); }); }); describe("update", () => { it("should update a project and log activity", async () => { const updateDto = { name: "Updated Project", status: ProjectStatus.ACTIVE, }; mockPrismaService.project.findUnique.mockResolvedValue(mockProject); mockPrismaService.project.update.mockResolvedValue({ ...mockProject, ...updateDto, }); mockActivityService.logProjectUpdated.mockResolvedValue({}); const result = await service.update( mockProjectId, mockWorkspaceId, mockUserId, updateDto ); expect(result.name).toBe("Updated Project"); expect(activityService.logProjectUpdated).toHaveBeenCalled(); }); it("should throw NotFoundException if project not found", async () => { mockPrismaService.project.findUnique.mockResolvedValue(null); await expect( service.update(mockProjectId, mockWorkspaceId, mockUserId, { name: "Test" }) ).rejects.toThrow(NotFoundException); }); it("should enforce workspace isolation when updating project", async () => { const otherWorkspaceId = "550e8400-e29b-41d4-a716-446655440099"; mockPrismaService.project.findUnique.mockResolvedValue(null); await expect( service.update(mockProjectId, otherWorkspaceId, mockUserId, { name: "Hacked" }) ).rejects.toThrow(NotFoundException); expect(prisma.project.findUnique).toHaveBeenCalledWith({ where: { id: mockProjectId, workspaceId: otherWorkspaceId }, }); }); }); describe("remove", () => { it("should delete a project and log activity", async () => { mockPrismaService.project.findUnique.mockResolvedValue(mockProject); mockPrismaService.project.delete.mockResolvedValue(mockProject); mockActivityService.logProjectDeleted.mockResolvedValue({}); await service.remove(mockProjectId, mockWorkspaceId, mockUserId); expect(prisma.project.delete).toHaveBeenCalled(); expect(activityService.logProjectDeleted).toHaveBeenCalled(); }); it("should throw NotFoundException if project not found", async () => { mockPrismaService.project.findUnique.mockResolvedValue(null); await expect( service.remove(mockProjectId, mockWorkspaceId, mockUserId) ).rejects.toThrow(NotFoundException); }); it("should enforce workspace isolation when deleting project", async () => { const otherWorkspaceId = "550e8400-e29b-41d4-a716-446655440099"; mockPrismaService.project.findUnique.mockResolvedValue(null); await expect( service.remove(mockProjectId, otherWorkspaceId, mockUserId) ).rejects.toThrow(NotFoundException); expect(prisma.project.findUnique).toHaveBeenCalledWith({ where: { id: mockProjectId, workspaceId: otherWorkspaceId }, }); }); }); describe("database constraint violations", () => { it("should handle unique constraint violations on create", async () => { const createDto = { name: "Duplicate Project", description: "Project description", }; const prismaError = new Prisma.PrismaClientKnownRequestError( "Unique constraint failed", { code: "P2002", clientVersion: "5.0.0", meta: { target: ["workspaceId", "name"], }, } ); mockPrismaService.project.create.mockRejectedValue(prismaError); await expect( service.create(mockWorkspaceId, mockUserId, createDto) ).rejects.toThrow(Prisma.PrismaClientKnownRequestError); }); it("should handle record not found on update (P2025)", async () => { mockPrismaService.project.findUnique.mockResolvedValue(mockProject); const prismaError = new Prisma.PrismaClientKnownRequestError( "Record to update not found", { code: "P2025", clientVersion: "5.0.0", } ); mockPrismaService.project.update.mockRejectedValue(prismaError); await expect( service.update(mockProjectId, mockWorkspaceId, mockUserId, { name: "Updated" }) ).rejects.toThrow(Prisma.PrismaClientKnownRequestError); }); }); });