Files
Nexus/packages/api/src/__tests__/comment-sanitization-router.test.ts
T
Hartmut b41c1d2501
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)
rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)

Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
2026-05-21 16:28:40 +02:00

146 lines
4.8 KiB
TypeScript

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<typeof import("../lib/comment-entity-registry.js")>();
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<string, unknown>, 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<typeof vi.fn>) {
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 <script> and </script> 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: "<script>alert(1)</script>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("<script>");
expect(callArg.data.body).not.toContain("</script>");
});
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 <b>bold</b> and <i>italic</i>",
});
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) — <b>please review</b>",
});
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");
});
});