fix(SEC-REVIEW-3): Add @MaxLength to SearchQueryDto.q for consistency
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

All other search DTOs (SemanticSearchBodyDto, HybridSearchBodyDto,
BrainQueryDto, BrainSearchDto) already enforce @MaxLength(500) on their
query fields. SearchQueryDto.q was missed, leaving the full-text
knowledge search endpoint accepting arbitrarily long queries.

Adds @MaxLength(500) decorator and validation test coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-06 14:39:08 -06:00
parent 433212e00f
commit 57441e2e64
3 changed files with 94 additions and 4 deletions

View File

@@ -0,0 +1,86 @@
import { describe, it, expect } from "vitest";
import { validate } from "class-validator";
import { plainToInstance } from "class-transformer";
import { SearchQueryDto } from "./search-query.dto";
/**
* Validation tests for SearchQueryDto
*
* Verifies that the full-text knowledge search endpoint
* enforces input length limits to prevent abuse.
*/
describe("SearchQueryDto - Input Validation", () => {
it("should pass validation with a valid query string", async () => {
const dto = plainToInstance(SearchQueryDto, {
q: "search term",
});
const errors = await validate(dto);
expect(errors).toHaveLength(0);
});
it("should pass validation with a query at exactly 500 characters", async () => {
const dto = plainToInstance(SearchQueryDto, {
q: "a".repeat(500),
});
const errors = await validate(dto);
expect(errors).toHaveLength(0);
});
it("should reject a query exceeding 500 characters", async () => {
const dto = plainToInstance(SearchQueryDto, {
q: "a".repeat(501),
});
const errors = await validate(dto);
expect(errors.length).toBeGreaterThan(0);
const qError = errors.find((e) => e.property === "q");
expect(qError).toBeDefined();
expect(qError!.constraints).toHaveProperty("maxLength");
expect(qError!.constraints!.maxLength).toContain("500");
});
it("should reject a missing q field", async () => {
const dto = plainToInstance(SearchQueryDto, {});
const errors = await validate(dto);
expect(errors.length).toBeGreaterThan(0);
const qError = errors.find((e) => e.property === "q");
expect(qError).toBeDefined();
});
it("should reject a non-string q field", async () => {
const dto = plainToInstance(SearchQueryDto, {
q: 12345,
});
const errors = await validate(dto);
expect(errors.length).toBeGreaterThan(0);
const qError = errors.find((e) => e.property === "q");
expect(qError).toBeDefined();
});
it("should pass validation with optional fields included", async () => {
const dto = plainToInstance(SearchQueryDto, {
q: "search term",
page: 1,
limit: 10,
});
const errors = await validate(dto);
expect(errors).toHaveLength(0);
});
it("should reject limit exceeding 100", async () => {
const dto = plainToInstance(SearchQueryDto, {
q: "search term",
limit: 101,
});
const errors = await validate(dto);
expect(errors.length).toBeGreaterThan(0);
const limitError = errors.find((e) => e.property === "limit");
expect(limitError).toBeDefined();
});
});

View File

@@ -7,6 +7,7 @@ import { EntryStatus } from "@prisma/client";
*/
export class SearchQueryDto {
@IsString({ message: "q (query) must be a string" })
@MaxLength(500, { message: "q must not exceed 500 characters" })
q!: string;
@IsOptional()