import { SystemRole } from "@nexus/shared"; import { describe, expect, it, vi } from "vitest"; import { commentRouter } from "../router/comment.js"; import { createCallerFactory } from "../trpc.js"; vi.mock("../lib/comment-entity-registry.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, assertCommentEntityAccess: vi.fn().mockResolvedValue({ listMentionCandidates: vi.fn(), buildLink: vi.fn().mockReturnValue("/estimates/est_1"), }), }; }); vi.mock("../lib/audit.js", () => ({ createAuditEntry: vi.fn().mockResolvedValue(undefined), })); vi.mock("../lib/create-notification.js", () => ({ createNotification: vi.fn().mockResolvedValue(undefined), })); const createCaller = createCallerFactory(commentRouter); function createContext(db: Record, role = SystemRole.MANAGER) { return { session: { user: { email: "mgr@example.com", name: "Manager", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_mgr", systemRole: role, permissionOverrides: null, }, }; } function makeDb(commentCreate: ReturnType) { return { estimate: { findUnique: vi.fn().mockResolvedValue({ id: "est_1" }), }, comment: { create: commentCreate, }, notification: { create: vi.fn(), }, auditLog: { create: vi.fn(), }, }; } describe("comment router — HTML sanitization before DB write", () => { it("strips script tags from comment body before writing to the database", async () => { // stripHtml removes the tags; the inner text "alert(1)" // is plain text and is therefore preserved — the HTML injection vector (the tags // themselves) is eliminated. const sanitizedBody = "alert(1)Hello"; const commentCreate = vi.fn().mockResolvedValue({ id: "comment_1", body: sanitizedBody, author: { id: "user_mgr", name: "Manager", email: "mgr@example.com", image: null }, }); const caller = createCaller(createContext(makeDb(commentCreate))); await caller.create({ entityType: "estimate", entityId: "est_1", body: "Hello", }); expect(commentCreate).toHaveBeenCalledOnce(); const callArg = commentCreate.mock.calls[0]![0] as { data: { body: string } }; // Tags are stripped — no angle brackets remain in what the DB receives expect(callArg.data.body).toBe(sanitizedBody); expect(callArg.data.body).not.toContain(""); }); it("strips bold and italic tags but preserves the text content", async () => { const commentCreate = vi.fn().mockResolvedValue({ id: "comment_2", body: "This is bold and italic", author: { id: "user_mgr", name: "Manager", email: "mgr@example.com", image: null }, }); const caller = createCaller(createContext(makeDb(commentCreate))); await caller.create({ entityType: "estimate", entityId: "est_1", body: "This is bold and italic", }); expect(commentCreate).toHaveBeenCalledOnce(); const callArg = commentCreate.mock.calls[0]![0] as { data: { body: string } }; expect(callArg.data.body).toBe("This is bold and italic"); }); it("passes plain text through unchanged", async () => { const commentCreate = vi.fn().mockResolvedValue({ id: "comment_3", body: "Just a plain comment", author: { id: "user_mgr", name: "Manager", email: "mgr@example.com", image: null }, }); const caller = createCaller(createContext(makeDb(commentCreate))); await caller.create({ entityType: "estimate", entityId: "est_1", body: "Just a plain comment", }); expect(commentCreate).toHaveBeenCalledOnce(); const callArg = commentCreate.mock.calls[0]![0] as { data: { body: string } }; expect(callArg.data.body).toBe("Just a plain comment"); }); it("strips HTML but preserves mention syntax and correctly populates the mentions array", async () => { const commentCreate = vi.fn().mockResolvedValue({ id: "comment_4", body: "Hi @[Alice](user_1) — please review", author: { id: "user_mgr", name: "Manager", email: "mgr@example.com", image: null }, }); const caller = createCaller(createContext(makeDb(commentCreate))); await caller.create({ entityType: "estimate", entityId: "est_1", body: "Hi @[Alice](user_1) — please review", }); expect(commentCreate).toHaveBeenCalledOnce(); const callArg = commentCreate.mock.calls[0]![0] as { data: { body: string; mentions: string[] }; }; expect(callArg.data.body).toBe("Hi @[Alice](user_1) — please review"); expect(callArg.data.mentions).toContain("user_1"); }); });