import { describe, expect, it, vi, beforeEach } from "vitest"; import { validate } from "class-validator"; import { plainToInstance } from "class-transformer"; import { BadRequestException } from "@nestjs/common"; import { BrainSearchDto, BrainQueryDto } from "./dto"; import { BrainService } from "./brain.service"; import { PrismaService } from "../prisma/prisma.service"; describe("Brain Search Validation", () => { describe("BrainSearchDto", () => { it("should accept a valid search query", async () => { const dto = plainToInstance(BrainSearchDto, { q: "meeting notes", limit: 10 }); const errors = await validate(dto); expect(errors).toHaveLength(0); }); it("should accept empty query params", async () => { const dto = plainToInstance(BrainSearchDto, {}); const errors = await validate(dto); expect(errors).toHaveLength(0); }); it("should reject search query exceeding 500 characters", async () => { const longQuery = "a".repeat(501); const dto = plainToInstance(BrainSearchDto, { q: longQuery }); const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); const qError = errors.find((e) => e.property === "q"); expect(qError).toBeDefined(); expect(qError?.constraints?.maxLength).toContain("500"); }); it("should accept search query at exactly 500 characters", async () => { const maxQuery = "a".repeat(500); const dto = plainToInstance(BrainSearchDto, { q: maxQuery }); const errors = await validate(dto); expect(errors).toHaveLength(0); }); it("should reject negative limit", async () => { const dto = plainToInstance(BrainSearchDto, { q: "test", limit: -1 }); const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); const limitError = errors.find((e) => e.property === "limit"); expect(limitError).toBeDefined(); expect(limitError?.constraints?.min).toContain("1"); }); it("should reject zero limit", async () => { const dto = plainToInstance(BrainSearchDto, { q: "test", limit: 0 }); const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); const limitError = errors.find((e) => e.property === "limit"); expect(limitError).toBeDefined(); }); it("should reject limit exceeding 100", async () => { const dto = plainToInstance(BrainSearchDto, { q: "test", limit: 101 }); const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); const limitError = errors.find((e) => e.property === "limit"); expect(limitError).toBeDefined(); expect(limitError?.constraints?.max).toContain("100"); }); it("should accept limit at boundaries (1 and 100)", async () => { const dto1 = plainToInstance(BrainSearchDto, { limit: 1 }); const errors1 = await validate(dto1); expect(errors1).toHaveLength(0); const dto100 = plainToInstance(BrainSearchDto, { limit: 100 }); const errors100 = await validate(dto100); expect(errors100).toHaveLength(0); }); it("should reject non-integer limit", async () => { const dto = plainToInstance(BrainSearchDto, { limit: 10.5 }); const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); const limitError = errors.find((e) => e.property === "limit"); expect(limitError).toBeDefined(); }); }); describe("BrainQueryDto search and query length validation", () => { it("should reject query exceeding 500 characters", async () => { const longQuery = "a".repeat(501); const dto = plainToInstance(BrainQueryDto, { workspaceId: "550e8400-e29b-41d4-a716-446655440000", query: longQuery, }); const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); const queryError = errors.find((e) => e.property === "query"); expect(queryError).toBeDefined(); expect(queryError?.constraints?.maxLength).toContain("500"); }); it("should reject search exceeding 500 characters", async () => { const longSearch = "b".repeat(501); const dto = plainToInstance(BrainQueryDto, { workspaceId: "550e8400-e29b-41d4-a716-446655440000", search: longSearch, }); const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); const searchError = errors.find((e) => e.property === "search"); expect(searchError).toBeDefined(); expect(searchError?.constraints?.maxLength).toContain("500"); }); it("should accept query at exactly 500 characters", async () => { const maxQuery = "a".repeat(500); const dto = plainToInstance(BrainQueryDto, { workspaceId: "550e8400-e29b-41d4-a716-446655440000", query: maxQuery, }); const errors = await validate(dto); expect(errors).toHaveLength(0); }); it("should accept search at exactly 500 characters", async () => { const maxSearch = "b".repeat(500); const dto = plainToInstance(BrainQueryDto, { workspaceId: "550e8400-e29b-41d4-a716-446655440000", search: maxSearch, }); const errors = await validate(dto); expect(errors).toHaveLength(0); }); }); describe("BrainService.search defensive validation", () => { let service: BrainService; let prisma: { task: { findMany: ReturnType }; event: { findMany: ReturnType }; project: { findMany: ReturnType }; }; beforeEach(() => { prisma = { task: { findMany: vi.fn().mockResolvedValue([]) }, event: { findMany: vi.fn().mockResolvedValue([]) }, project: { findMany: vi.fn().mockResolvedValue([]) }, }; service = new BrainService(prisma as unknown as PrismaService); }); it("should throw BadRequestException for search term exceeding 500 characters", async () => { const longTerm = "x".repeat(501); await expect(service.search("workspace-id", longTerm)).rejects.toThrow(BadRequestException); await expect(service.search("workspace-id", longTerm)).rejects.toThrow("500"); }); it("should accept search term at exactly 500 characters", async () => { const maxTerm = "x".repeat(500); await expect(service.search("workspace-id", maxTerm)).resolves.toBeDefined(); }); it("should clamp limit to max 100 when higher value provided", async () => { await service.search("workspace-id", "test", 200); expect(prisma.task.findMany).toHaveBeenCalledWith(expect.objectContaining({ take: 100 })); }); it("should clamp limit to min 1 when negative value provided", async () => { await service.search("workspace-id", "test", -5); expect(prisma.task.findMany).toHaveBeenCalledWith(expect.objectContaining({ take: 1 })); }); it("should clamp limit to min 1 when zero provided", async () => { await service.search("workspace-id", "test", 0); expect(prisma.task.findMany).toHaveBeenCalledWith(expect.objectContaining({ take: 1 })); }); it("should pass through valid limit values unchanged", async () => { await service.search("workspace-id", "test", 50); expect(prisma.task.findMany).toHaveBeenCalledWith(expect.objectContaining({ take: 50 })); }); }); describe("BrainService.query defensive validation", () => { let service: BrainService; let prisma: { task: { findMany: ReturnType }; event: { findMany: ReturnType }; project: { findMany: ReturnType }; }; beforeEach(() => { prisma = { task: { findMany: vi.fn().mockResolvedValue([]) }, event: { findMany: vi.fn().mockResolvedValue([]) }, project: { findMany: vi.fn().mockResolvedValue([]) }, }; service = new BrainService(prisma as unknown as PrismaService); }); it("should throw BadRequestException for search field exceeding 500 characters", async () => { const longSearch = "y".repeat(501); await expect( service.query({ workspaceId: "workspace-id", search: longSearch }) ).rejects.toThrow(BadRequestException); }); it("should throw BadRequestException for query field exceeding 500 characters", async () => { const longQuery = "z".repeat(501); await expect( service.query({ workspaceId: "workspace-id", query: longQuery }) ).rejects.toThrow(BadRequestException); }); it("should clamp limit to max 100 in query method", async () => { await service.query({ workspaceId: "workspace-id", limit: 200 }); expect(prisma.task.findMany).toHaveBeenCalledWith(expect.objectContaining({ take: 100 })); }); it("should clamp limit to min 1 in query method when negative", async () => { await service.query({ workspaceId: "workspace-id", limit: -10 }); expect(prisma.task.findMany).toHaveBeenCalledWith(expect.objectContaining({ take: 1 })); }); it("should accept valid query and search within limits", async () => { await expect( service.query({ workspaceId: "workspace-id", query: "test query", search: "test search", limit: 50, }) ).resolves.toBeDefined(); }); }); });