Files
buzz-sheet/lib/comments/admin-moderation.test.ts
gunshiz b9601b739d
CI / Verify (push) Successful in 1m50s
CI / Build immutable images and deploy (push) Successful in 3m47s
feat : imrpove admin comment
2026-10-08 15:06:19 +07:00

111 lines
6.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const state = vi.hoisted(() => ({
comment: {} as Record<string, unknown>, author: {} as Record<string, unknown>,
rootHidden: false, trashed: false, deletes: vi.fn(), updates: vi.fn(), audit: vi.fn(), changed: vi.fn(),
}));
vi.mock("server-only", () => ({}));
vi.mock("@/lib/notifications/events", () => ({ notifyNotificationChange: vi.fn() }));
vi.mock("@/lib/comments/events", () => ({ notifyCommentChange: state.changed }));
vi.mock("@/lib/auth/server", () => ({ getCustomerSession: vi.fn() }));
vi.mock("@/lib/audit-log", () => ({ auditActor: (actor: unknown) => actor, writeAuditLog: state.audit }));
vi.mock("@/lib/media/storage", () => ({ getMediaStorage: vi.fn(), publicMediaUrl: vi.fn() }));
vi.mock("@/db", () => ({ getDb: () => database }));
import { comments, guides, sessions, users } from "@/db/schema";
import { mutateComment } from "./repository";
import { publishComment } from "./publish";
const id = "72df08ab-50dd-4cbd-9a69-70949d34cf9f";
const guideId = "72df08ab-50dd-4cbd-9a69-70949d34cf9e";
const admin = { id: "admin", admin: true };
const reader = { id: "reader", admin: false };
function select(fields: Record<string, unknown>) {
let table: unknown;
const rows = () => {
if (table === comments) return fields.comment
? [{ comment: state.comment, thread: { id: "thread", guideId } }]
: [{ hidden: state.rootHidden }];
if (table === guides) return [{ id: guideId, name: "Amber", slug: "amber", public: true, trashedAt: state.trashed ? new Date() : null }];
if (table === users) return fields.role || fields.banned ? [state.author] : [{ id: admin.id, name: "Admin" }];
return [];
};
const query = {
from(value: unknown) { table = value; return query; },
innerJoin() { return query; }, where() { return query; }, limit() { return query; }, for() { return query; },
then(resolve: (value: unknown[]) => unknown) { return Promise.resolve(rows()).then(resolve); },
};
return query;
}
const database = {
transaction: async (task: (tx: unknown) => unknown) => task(database), select,
selectDistinct: select,
update: (table: unknown) => ({ set: (value: Record<string, unknown>) => ({ where: async () => {
state.updates(table, value);
if (table === users) Object.assign(state.author, value);
} }) }),
delete: (table: unknown) => ({ where: async () => { state.deletes(table); } }),
};
beforeEach(() => {
state.comment = { id, authorId: reader.id, threadId: "thread", rootId: null, hidden: false, deletedAt: null };
state.author = { id: reader.id, email: "[email protected]", role: "user", emailVerified: true, banned: false };
state.rootHidden = false; state.trashed = false;
state.deletes.mockReset(); state.updates.mockReset(); state.audit.mockReset(); state.changed.mockReset();
});
describe("admin comment deletion", () => {
it.each(["visible", "hidden", "hidden root", "deleted placeholder"])("allows an admin to delete another author's %s comment and records it", async (visibility) => {
state.comment.hidden = visibility === "hidden";
state.comment.deletedAt = visibility === "deleted placeholder" ? new Date() : null;
if (visibility === "hidden root") { state.comment.rootId = id; state.rootHidden = true; }
await mutateComment(id, admin, "delete");
expect(state.deletes).toHaveBeenCalledWith(comments);
expect(state.audit).toHaveBeenCalledWith(database, expect.anything(), expect.objectContaining({ action: "comment.deleted", targetId: id }));
expect(state.changed).toHaveBeenCalledWith(`guide:${guideId}`);
});
it("rejects deletion by another regular user", async () => {
await expect(mutateComment(id, { id: "other", admin: false }, "delete")).rejects.toMatchObject({ status: 403 });
expect(state.deletes).not.toHaveBeenCalled();
});
it("retains author deletion", async () => {
await mutateComment(id, reader, "delete");
expect(state.deletes).toHaveBeenCalledWith(comments);
});
it("rejects deletion for a trashed guide", async () => {
state.trashed = true;
await expect(mutateComment(id, admin, "delete")).rejects.toMatchObject({ status: 409 });
expect(state.deletes).not.toHaveBeenCalled();
});
});
describe("account bans from comment moderation", () => {
it("sets Better Auth's ban fields, revokes sessions, and audits without deleting comments", async () => {
await mutateComment(id, admin, "ban", true);
expect(state.updates).toHaveBeenCalledWith(users, { banned: true, banReason: "Banned by comment moderation", banExpires: null });
expect(state.deletes).toHaveBeenCalledWith(sessions);
expect(state.deletes).not.toHaveBeenCalledWith(comments);
expect(state.audit).toHaveBeenCalledWith(database, expect.anything(), expect.objectContaining({ action: "comment.author_banned" }));
});
it("clears the ban and audits without revoking sessions", async () => {
state.author.banned = true;
await mutateComment(id, admin, "ban", false);
expect(state.author).toMatchObject({ banned: false, banReason: null, banExpires: null });
expect(state.deletes).not.toHaveBeenCalled();
expect(state.audit).toHaveBeenCalledWith(database, expect.anything(), expect.objectContaining({ action: "comment.author_unbanned" }));
});
it.each(["regular user", "self", "another admin"])("rejects bans for %s", async (scenario) => {
const actor = scenario === "regular user" ? reader : admin;
if (scenario === "self") { state.author.id = admin.id; state.comment.authorId = admin.id; }
if (scenario === "another admin") state.author.role = "admin";
await expect(mutateComment(id, actor, "ban", true)).rejects.toMatchObject({ status: 403 });
expect(state.updates).not.toHaveBeenCalled(); expect(state.deletes).not.toHaveBeenCalled();
});
it.each(["new comment", "reply", "edit"])("rejects a banned author's %s before processing uploads", async (kind) => {
state.author.banned = true;
const body = new FormData(); body.set("text", "New comment");
if (kind === "reply") body.set("replyToId", id);
await expect(publishComment(new Request("https://guide.example.test/api/comments", { method: "POST", body }), reader,
kind === "edit" ? { id } : { target: `guide:${guideId}` })).rejects.toMatchObject({ status: 403, message: "comment-author-banned" });
expect(state.updates).not.toHaveBeenCalled();
});
});