feat(comments) : filter abusive language and prevent spam
CI / Verify (push) Successful in 1m41s
CI / Build immutable images and deploy (push) Successful in 2m30s

This commit is contained in:
2026-10-08 04:45:45 +07:00 Unverified
parent 0f0e983bcb
commit 20c52fac04
8 changed files with 235 additions and 44 deletions
+24
View File
@@ -14,6 +14,7 @@ vi.mock("@/lib/comments/events", () => ({ notifyCommentChange: notify }));
const storage = vi.hoisted(() => ({ write: vi.fn(), delete: vi.fn() }));
const rateLimit = vi.hoisted(() => vi.fn());
vi.mock("@/lib/security/rate-limit", () => ({ limitRequest: rateLimit }));
vi.mock("@/lib/redis/client", () => ({ getRedisClient: async () => ({ set: async () => "OK", eval: async () => 1 }) }));
const session = vi.hoisted(() => ({ get: vi.fn() }));
vi.mock("@/lib/media/storage", () => ({ getMediaStorage: async () => storage, publicMediaUrl: (key: string) => `https://storage.example.test/${key}` }));
vi.mock("@/lib/auth/server", () => ({ getCustomerSession: session.get }));
@@ -380,6 +381,29 @@ describeDatabase("guide comments against PostgreSQL", { timeout: 30000 }, () =>
expect(read.status).toBe(200); expect(read.headers.get("cache-control")).toContain("no-store");
expect((await read.json()).items).toHaveLength(1);
});
it("rejects abusive posts, replies and edits before uploads, revisions or notifications", async () => {
session.get.mockResolvedValue({ user: { id: author.id }, session: { id: "session" } });
const url = `https://guide.example.test/api/comments?target=${target}`;
const blocked = await commentsRoute.POST(request("f.u.c.k", { images: [png] }, url));
expect(blocked.status).toBe(400);
expect(await blocked.json()).toEqual({ error: "comment-abusive-language" });
expect((await page()).items).toHaveLength(0);
expect(storage.write).not.toHaveBeenCalled();
expect(notify).not.toHaveBeenCalled();
const root = await post("Normal discussion");
notify.mockClear();
vi.mocked(after).mockClear();
const reply = await commentsRoute.POST(request("ไอ้เหี้ย", { reply: root.id }, url));
expect(reply.status).toBe(400);
const edited = await itemRoute.PATCH(request("buy now ".repeat(8), { version: 1, images: [png] }), { params: Promise.resolve({ id: root.id }) });
expect(edited.status).toBe(400);
expect(await edited.json()).toEqual({ error: "comment-spam" });
expect((await page()).items).toHaveLength(1);
expect((await commentHistory(root.id, author)).items).toHaveLength(1);
expect(storage.write).not.toHaveBeenCalled();
expect(notify).not.toHaveBeenCalled();
expect(after).not.toHaveBeenCalled();
});
it("enforces author and admin rights through mutation endpoints", async () => {
const root = await post();
session.get.mockResolvedValue({ user: { id: other.id }, session: { id: "session" } });