import { Test, TestingModule } from "@nestjs/testing"; import { describe, it, expect, beforeEach, vi } from "vitest"; import { BadRequestException, NotFoundException } from "@nestjs/common"; import { FindingsService } from "./findings.service"; import { PrismaService } from "../prisma/prisma.service"; import { EmbeddingService } from "../knowledge/services/embedding.service"; describe("FindingsService", () => { let service: FindingsService; let prisma: PrismaService; let embeddingService: EmbeddingService; const mockWorkspaceId = "550e8400-e29b-41d4-a716-446655440001"; const mockFindingId = "550e8400-e29b-41d4-a716-446655440002"; const mockTaskId = "550e8400-e29b-41d4-a716-446655440003"; const mockPrismaService = { finding: { create: vi.fn(), findMany: vi.fn(), findUnique: vi.fn(), count: vi.fn(), delete: vi.fn(), }, agentTask: { findUnique: vi.fn(), }, $queryRaw: vi.fn(), $executeRaw: vi.fn(), }; const mockEmbeddingService = { isConfigured: vi.fn(), generateEmbedding: vi.fn(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ FindingsService, { provide: PrismaService, useValue: mockPrismaService, }, { provide: EmbeddingService, useValue: mockEmbeddingService, }, ], }).compile(); service = module.get(FindingsService); prisma = module.get(PrismaService); embeddingService = module.get(EmbeddingService); vi.clearAllMocks(); }); it("should be defined", () => { expect(service).toBeDefined(); }); describe("create", () => { it("should create a finding and store embedding when configured", async () => { const createDto = { taskId: mockTaskId, agentId: "research-agent", type: "security", title: "SQL injection risk", data: { severity: "high" }, summary: "Potential SQL injection in search endpoint.", }; const createdFinding = { id: mockFindingId, workspaceId: mockWorkspaceId, ...createDto, createdAt: new Date(), updatedAt: new Date(), }; mockPrismaService.agentTask.findUnique.mockResolvedValue({ id: mockTaskId, workspaceId: mockWorkspaceId, }); mockPrismaService.finding.create.mockResolvedValue(createdFinding); mockPrismaService.finding.findUnique.mockResolvedValue(createdFinding); mockEmbeddingService.isConfigured.mockReturnValue(true); mockEmbeddingService.generateEmbedding.mockResolvedValue([0.1, 0.2, 0.3]); mockPrismaService.$executeRaw.mockResolvedValue(1); const result = await service.create(mockWorkspaceId, createDto); expect(result).toEqual(createdFinding); expect(prisma.finding.create).toHaveBeenCalledWith({ data: expect.objectContaining({ workspaceId: mockWorkspaceId, taskId: mockTaskId, agentId: "research-agent", type: "security", title: "SQL injection risk", }), select: expect.any(Object), }); expect(embeddingService.generateEmbedding).toHaveBeenCalledWith(createDto.summary); expect(prisma.$executeRaw).toHaveBeenCalled(); }); it("should create a finding without embedding when not configured", async () => { const createDto = { agentId: "research-agent", type: "security", title: "SQL injection risk", data: { severity: "high" }, summary: "Potential SQL injection in search endpoint.", }; const createdFinding = { id: mockFindingId, workspaceId: mockWorkspaceId, taskId: null, ...createDto, createdAt: new Date(), updatedAt: new Date(), }; mockPrismaService.finding.create.mockResolvedValue(createdFinding); mockEmbeddingService.isConfigured.mockReturnValue(false); const result = await service.create(mockWorkspaceId, createDto); expect(result).toEqual(createdFinding); expect(embeddingService.generateEmbedding).not.toHaveBeenCalled(); expect(prisma.$executeRaw).not.toHaveBeenCalled(); }); }); describe("findAll", () => { it("should return paginated findings with filters", async () => { const findings = [ { id: mockFindingId, workspaceId: mockWorkspaceId, taskId: null, agentId: "research-agent", type: "security", title: "SQL injection risk", data: { severity: "high" }, summary: "Potential SQL injection in search endpoint.", createdAt: new Date(), updatedAt: new Date(), }, ]; mockPrismaService.finding.findMany.mockResolvedValue(findings); mockPrismaService.finding.count.mockResolvedValue(1); const result = await service.findAll(mockWorkspaceId, { page: 1, limit: 10, type: "security", agentId: "research-agent", }); expect(result).toEqual({ data: findings, meta: { total: 1, page: 1, limit: 10, totalPages: 1, }, }); expect(prisma.finding.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: { workspaceId: mockWorkspaceId, type: "security", agentId: "research-agent", }, }) ); }); }); describe("findOne", () => { it("should return a finding", async () => { const finding = { id: mockFindingId, workspaceId: mockWorkspaceId, taskId: null, agentId: "research-agent", type: "security", title: "SQL injection risk", data: { severity: "high" }, summary: "Potential SQL injection in search endpoint.", createdAt: new Date(), updatedAt: new Date(), }; mockPrismaService.finding.findUnique.mockResolvedValue(finding); const result = await service.findOne(mockFindingId, mockWorkspaceId); expect(result).toEqual(finding); expect(prisma.finding.findUnique).toHaveBeenCalledWith({ where: { id: mockFindingId, workspaceId: mockWorkspaceId, }, select: expect.any(Object), }); }); it("should throw when finding does not exist", async () => { mockPrismaService.finding.findUnique.mockResolvedValue(null); await expect(service.findOne(mockFindingId, mockWorkspaceId)).rejects.toThrow( NotFoundException ); }); }); describe("search", () => { it("should throw BadRequestException when embeddings are not configured", async () => { mockEmbeddingService.isConfigured.mockReturnValue(false); await expect( service.search(mockWorkspaceId, { query: "sql injection", }) ).rejects.toThrow(BadRequestException); }); it("should return similarity-ranked search results", async () => { mockEmbeddingService.isConfigured.mockReturnValue(true); mockEmbeddingService.generateEmbedding.mockResolvedValue([0.1, 0.2, 0.3]); mockPrismaService.$queryRaw .mockResolvedValueOnce([ { id: mockFindingId, workspace_id: mockWorkspaceId, task_id: null, agent_id: "research-agent", type: "security", title: "SQL injection risk", data: { severity: "high" }, summary: "Potential SQL injection in search endpoint.", created_at: new Date(), updated_at: new Date(), score: 0.91, }, ]) .mockResolvedValueOnce([{ count: BigInt(1) }]); const result = await service.search(mockWorkspaceId, { query: "sql injection", page: 1, limit: 5, similarityThreshold: 0.5, }); expect(result.query).toBe("sql injection"); expect(result.data).toHaveLength(1); expect(result.data[0].score).toBe(0.91); expect(result.meta.total).toBe(1); expect(prisma.$queryRaw).toHaveBeenCalledTimes(2); }); }); describe("remove", () => { it("should delete a finding", async () => { mockPrismaService.finding.findUnique.mockResolvedValue({ id: mockFindingId, workspaceId: mockWorkspaceId, }); mockPrismaService.finding.delete.mockResolvedValue({ id: mockFindingId, }); const result = await service.remove(mockFindingId, mockWorkspaceId); expect(result).toEqual({ message: "Finding deleted successfully" }); expect(prisma.finding.delete).toHaveBeenCalledWith({ where: { id: mockFindingId, workspaceId: mockWorkspaceId, }, }); }); it("should throw when finding does not exist", async () => { mockPrismaService.finding.findUnique.mockResolvedValue(null); await expect(service.remove(mockFindingId, mockWorkspaceId)).rejects.toThrow( NotFoundException ); }); }); });