import { beforeAll, beforeEach, describe, expect, it, afterAll, vi } from "vitest"; import { randomUUID as uuid } from "crypto"; import { Test, TestingModule } from "@nestjs/testing"; import { BadRequestException, NotFoundException } from "@nestjs/common"; import { PrismaClient, Prisma } from "@prisma/client"; import { FindingsService } from "./findings.service"; import { PrismaService } from "../prisma/prisma.service"; import { EmbeddingService } from "../knowledge/services/embedding.service"; const shouldRunDbIntegrationTests = process.env.RUN_DB_TESTS === "true" && Boolean(process.env.DATABASE_URL); const describeFn = shouldRunDbIntegrationTests ? describe : describe.skip; const EMBEDDING_DIMENSION = 1536; function vector(value: number): number[] { return Array.from({ length: EMBEDDING_DIMENSION }, () => value); } function toVectorLiteral(input: number[]): string { return `[${input.join(",")}]`; } describeFn("FindingsService Integration", () => { let moduleRef: TestingModule; let prisma: PrismaClient; let service: FindingsService; let workspaceId: string; let ownerId: string; let setupComplete = false; const embeddingServiceMock = { isConfigured: vi.fn(), generateEmbedding: vi.fn(), }; beforeAll(async () => { prisma = new PrismaClient(); await prisma.$connect(); const workspace = await prisma.workspace.create({ data: { name: `Findings Integration ${Date.now()}`, owner: { create: { email: `findings-integration-${Date.now()}@example.com`, name: "Findings Integration Owner", }, }, }, }); workspaceId = workspace.id; ownerId = workspace.ownerId; moduleRef = await Test.createTestingModule({ providers: [ FindingsService, { provide: PrismaService, useValue: prisma, }, { provide: EmbeddingService, useValue: embeddingServiceMock, }, ], }).compile(); service = moduleRef.get(FindingsService); setupComplete = true; }); beforeEach(() => { vi.clearAllMocks(); embeddingServiceMock.isConfigured.mockReturnValue(false); }); afterAll(async () => { if (!prisma) { return; } if (workspaceId) { await prisma.finding.deleteMany({ where: { workspaceId } }); await prisma.workspace.deleteMany({ where: { id: workspaceId } }); } if (ownerId) { await prisma.user.deleteMany({ where: { id: ownerId } }); } if (moduleRef) { await moduleRef.close(); } await prisma.$disconnect(); }); it("creates, lists, fetches, and deletes findings", async () => { if (!setupComplete) { return; } const created = await service.create(workspaceId, { agentId: "agent-findings-crud", type: "security", title: "Unescaped SQL fragment", data: { severity: "high" }, summary: "Potential injection risk in dynamic query path.", }); expect(created.id).toBeDefined(); expect(created.workspaceId).toBe(workspaceId); expect(created.taskId).toBeNull(); const listed = await service.findAll(workspaceId, { page: 1, limit: 10, agentId: "agent-findings-crud", }); expect(listed.meta.total).toBeGreaterThanOrEqual(1); expect(listed.data.some((row) => row.id === created.id)).toBe(true); const found = await service.findOne(created.id, workspaceId); expect(found.id).toBe(created.id); expect(found.title).toBe("Unescaped SQL fragment"); await expect(service.findOne(created.id, uuid())).rejects.toThrow(NotFoundException); await expect(service.remove(created.id, workspaceId)).resolves.toEqual({ message: "Finding deleted successfully", }); await expect(service.findOne(created.id, workspaceId)).rejects.toThrow(NotFoundException); }); it("rejects create when taskId does not exist in workspace", async () => { if (!setupComplete) { return; } await expect( service.create(workspaceId, { taskId: uuid(), agentId: "agent-findings-missing-task", type: "bug", title: "Invalid task id", data: { source: "integration-test" }, summary: "Should fail when task relation is not found.", }) ).rejects.toThrow(NotFoundException); }); it("rejects vector search when embeddings are disabled", async () => { if (!setupComplete) { return; } embeddingServiceMock.isConfigured.mockReturnValue(false); await expect( service.search(workspaceId, { query: "security issue", }) ).rejects.toThrow(BadRequestException); }); it("searches findings by vector similarity with filters", async () => { if (!setupComplete) { return; } const near = vector(0.01); const far = vector(0.9); const matchedFinding = await prisma.finding.create({ data: { workspaceId, agentId: "agent-findings-search-a", type: "incident", title: "Authentication bypass", data: { score: 0.9 } as Prisma.InputJsonValue, summary: "Bypass risk found in login checks.", }, }); const otherFinding = await prisma.finding.create({ data: { workspaceId, agentId: "agent-findings-search-b", type: "incident", title: "Retry timeout", data: { score: 0.2 } as Prisma.InputJsonValue, summary: "Timeout issue in downstream retries.", }, }); await prisma.$executeRaw` UPDATE findings SET embedding = ${toVectorLiteral(near)}::vector(1536) WHERE id = ${matchedFinding.id}::uuid `; await prisma.$executeRaw` UPDATE findings SET embedding = ${toVectorLiteral(far)}::vector(1536) WHERE id = ${otherFinding.id}::uuid `; embeddingServiceMock.isConfigured.mockReturnValue(true); embeddingServiceMock.generateEmbedding.mockResolvedValue(near); const result = await service.search(workspaceId, { query: "authentication bypass risk", agentId: "agent-findings-search-a", limit: 10, similarityThreshold: 0, }); expect(result.query).toBe("authentication bypass risk"); expect(result.meta.total).toBe(1); expect(result.data).toHaveLength(1); expect(result.data[0]?.id).toBe(matchedFinding.id); expect(result.data[0]?.agentId).toBe("agent-findings-search-a"); expect(result.data.find((row) => row.id === otherFinding.id)).toBeUndefined(); }); });