test(api): fill router auth and security coverage gaps
Four new test files — 27 tests total: - role-router-auth.test.ts (8): UNAUTHORIZED/FORBIDDEN on all mutations for unauthenticated/USER callers; MANAGER and ADMIN happy paths - webhook-router-auth.test.ts (6): adminProcedure guard verified for all six webhook procedures across USER/MANAGER/ADMIN roles - comment-sanitization-router.test.ts (4): proves stripHtml runs before db.comment.create — script tags stripped, plain text and @mentions preserved - auth-anomaly-check/route.test.ts (+5 unit tests): detectAuthAnomalies() unit coverage — empty window, global threshold, per-entity threshold, null entityId, and both anomaly types firing simultaneously Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import { SystemRole } from "@capakraken/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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user