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) Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com> Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
146 lines
4.8 KiB
TypeScript
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");
|
|
});
|
|
});
|