chore: upgrade Node.js runtime to v24 across codebase #419
86
apps/api/src/knowledge/dto/search-query.dto.spec.ts
Normal file
86
apps/api/src/knowledge/dto/search-query.dto.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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()
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { marked } from "marked";
|
||||
import { gfmHeadingId } from "marked-gfm-heading-id";
|
||||
import { markedHighlight } from "marked-highlight";
|
||||
import hljs from "highlight.js";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
|
||||
const logger = new Logger("MarkdownRenderer");
|
||||
|
||||
/**
|
||||
* Configure marked with GFM, syntax highlighting, and security features
|
||||
*/
|
||||
@@ -199,8 +202,8 @@ export async function renderMarkdown(markdown: string): Promise<string> {
|
||||
return safeHtml;
|
||||
} catch (error) {
|
||||
// Log error but don't expose internal details
|
||||
console.error("Markdown rendering error:", error);
|
||||
throw new Error("Failed to render markdown content");
|
||||
logger.error("Markdown rendering error:", error);
|
||||
throw new Error("Failed to render markdown content", { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,8 +228,8 @@ export function renderMarkdownSync(markdown: string): string {
|
||||
|
||||
return safeHtml;
|
||||
} catch (error) {
|
||||
console.error("Markdown rendering error:", error);
|
||||
throw new Error("Failed to render markdown content");
|
||||
logger.error("Markdown rendering error:", error);
|
||||
throw new Error("Failed to render markdown content", { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user