/** * Tests for admin-moderation DB-layer (server/db/moderation.ts). * * Covers: * - listModerationQueue: merges posts + comments, sorts by reportedAt desc * - dismissModerationItem: clears flag, writes audit-log * - deleteModerationItem: soft-deletes content, persists snapshot * - banUserFromModerationItem: sets Profile.banned, writes audit-log * * Strategy: prisma-mock via vi.hoisted (analog demographics.patch.test.ts). */ import { describe, expect, it, vi, beforeEach } from "vitest"; const prismaMock = vi.hoisted(() => ({ communityPost: { findMany: vi.fn(), findUnique: vi.fn(), update: vi.fn(), }, communityReply: { findMany: vi.fn(), findUnique: vi.fn(), update: vi.fn(), }, profile: { update: vi.fn(), }, moderationAction: { create: vi.fn(), }, })); vi.mock("../../server/utils/prisma", () => ({ usePrisma: () => prismaMock, })); import { listModerationQueue, dismissModerationItem, deleteModerationItem, banUserFromModerationItem, } from "../../server/db/moderation"; beforeEach(() => { vi.clearAllMocks(); }); // ─── listModerationQueue ───────────────────────────────────────────────────── describe("listModerationQueue — merges posts + comments by reportedAt desc", () => { it("returns merged sorted list with newest report first", async () => { prismaMock.communityPost.findMany.mockResolvedValueOnce([ { id: "post-1", userId: "user-1", content: "bad post 1", reportedAt: new Date("2026-05-01T10:00:00Z"), createdAt: new Date("2026-04-30T10:00:00Z"), isDeleted: false, author: { id: "user-1", nickname: "alice", avatar: null, plan: "free", }, }, ]); prismaMock.communityReply.findMany.mockResolvedValueOnce([ { id: "reply-1", postId: "post-99", userId: "user-2", content: "bad comment 1", reportedAt: new Date("2026-05-02T10:00:00Z"), createdAt: new Date("2026-05-01T10:00:00Z"), isDeleted: false, author: { id: "user-2", nickname: "bob", avatar: null, plan: "pro", }, }, ]); const result = await listModerationQueue({ limit: 10 }); expect(result.items).toHaveLength(2); // reply has newer reportedAt → first expect(result.items[0]!.type).toBe("comment"); expect(result.items[0]!.id).toBe("reply-1"); expect(result.items[0]!.postId).toBe("post-99"); expect(result.items[1]!.type).toBe("post"); expect(result.items[1]!.id).toBe("post-1"); expect(result.nextCursor).toBeNull(); }); it("emits nextCursor when posts overflow limit", async () => { // limit=1, posts.length=2 → nextCursor non-null prismaMock.communityPost.findMany.mockResolvedValueOnce([ { id: "post-1", userId: "user-1", content: "p1", reportedAt: new Date("2026-05-01T10:00:00Z"), createdAt: new Date("2026-04-30T10:00:00Z"), isDeleted: false, author: null, }, { id: "post-2", userId: "user-1", content: "p2", reportedAt: new Date("2026-04-29T10:00:00Z"), createdAt: new Date("2026-04-29T10:00:00Z"), isDeleted: false, author: null, }, ]); prismaMock.communityReply.findMany.mockResolvedValueOnce([]); const result = await listModerationQueue({ limit: 1 }); expect(result.items).toHaveLength(1); expect(result.items[0]!.id).toBe("post-1"); expect(result.nextCursor).toBe("post:post-1"); }); it("filters where: { isModerated: true } on both tables", async () => { prismaMock.communityPost.findMany.mockResolvedValueOnce([]); prismaMock.communityReply.findMany.mockResolvedValueOnce([]); await listModerationQueue(); expect(prismaMock.communityPost.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: { isModerated: true }, }), ); expect(prismaMock.communityReply.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: { isModerated: true }, }), ); }); }); // ─── dismissModerationItem ─────────────────────────────────────────────────── describe("dismissModerationItem — flag clear", () => { it("sets isModerated=false on post + writes audit-log dismiss", async () => { prismaMock.communityPost.findUnique.mockResolvedValueOnce({ id: "post-1", content: "original content", }); prismaMock.communityPost.update.mockResolvedValueOnce({}); prismaMock.moderationAction.create.mockResolvedValueOnce({}); const result = await dismissModerationItem("post", "post-1", "admin-1"); expect(result).toEqual({ ok: true }); expect(prismaMock.communityPost.update).toHaveBeenCalledWith({ where: { id: "post-1" }, data: { isModerated: false, reportedAt: null }, }); expect(prismaMock.moderationAction.create).toHaveBeenCalledWith({ data: expect.objectContaining({ targetType: "post", targetId: "post-1", action: "dismiss", adminUserId: "admin-1", contentSnapshot: "original content", }), }); }); it("404 when target post not found", async () => { prismaMock.communityPost.findUnique.mockResolvedValueOnce(null); await expect( dismissModerationItem("post", "nonexistent", null), ).rejects.toMatchObject({ statusCode: 404 }); expect(prismaMock.communityPost.update).not.toHaveBeenCalled(); expect(prismaMock.moderationAction.create).not.toHaveBeenCalled(); }); it("dispatches to communityReply when type=comment", async () => { prismaMock.communityReply.findUnique.mockResolvedValueOnce({ id: "reply-1", content: "bad reply", }); prismaMock.communityReply.update.mockResolvedValueOnce({}); prismaMock.moderationAction.create.mockResolvedValueOnce({}); await dismissModerationItem("comment", "reply-1", null); expect(prismaMock.communityReply.update).toHaveBeenCalledWith({ where: { id: "reply-1" }, data: { isModerated: false, reportedAt: null }, }); expect(prismaMock.communityPost.update).not.toHaveBeenCalled(); }); }); // ─── deleteModerationItem ──────────────────────────────────────────────────── describe("deleteModerationItem — soft-delete with audit-snapshot", () => { it("scrubs content + sets isDeleted=true + persists original in audit-log", async () => { prismaMock.communityPost.findUnique.mockResolvedValueOnce({ id: "post-1", content: "this is the original toxic content", }); prismaMock.communityPost.update.mockResolvedValueOnce({}); prismaMock.moderationAction.create.mockResolvedValueOnce({}); await deleteModerationItem("post", "post-1", "admin-1", "Hass-Rede"); expect(prismaMock.communityPost.update).toHaveBeenCalledWith({ where: { id: "post-1" }, data: expect.objectContaining({ content: "", isDeleted: true, deletedAt: expect.any(Date), }), }); expect(prismaMock.moderationAction.create).toHaveBeenCalledWith({ data: expect.objectContaining({ targetType: "post", targetId: "post-1", action: "delete", adminUserId: "admin-1", contentSnapshot: "this is the original toxic content", reason: "Hass-Rede", }), }); }); it("404 when target not found", async () => { prismaMock.communityPost.findUnique.mockResolvedValueOnce(null); await expect( deleteModerationItem("post", "nonexistent", null), ).rejects.toMatchObject({ statusCode: 404 }); }); }); // ─── banUserFromModerationItem ─────────────────────────────────────────────── describe("banUserFromModerationItem — sets Profile.banned + audit-log", () => { it("patches Profile.banned=true, writes audit-log ban_user", async () => { prismaMock.communityPost.findUnique.mockResolvedValueOnce({ id: "post-1", content: "violating content", userId: "user-99", }); prismaMock.profile.update.mockResolvedValueOnce({}); prismaMock.moderationAction.create.mockResolvedValueOnce({}); const result = await banUserFromModerationItem( "post", "post-1", "admin-1", "wiederholter Verstoß", ); expect(result).toEqual({ ok: true, bannedUserId: "user-99" }); expect(prismaMock.profile.update).toHaveBeenCalledWith({ where: { id: "user-99" }, data: expect.objectContaining({ banned: true, bannedAt: expect.any(Date), bannedReason: "wiederholter Verstoß", }), }); expect(prismaMock.moderationAction.create).toHaveBeenCalledWith({ data: expect.objectContaining({ action: "ban_user", contentSnapshot: "violating content", reason: "wiederholter Verstoß", }), }); }); it("uses default ban-reason when none provided", async () => { prismaMock.communityReply.findUnique.mockResolvedValueOnce({ id: "reply-1", content: "bad", userId: "user-99", }); prismaMock.profile.update.mockResolvedValueOnce({}); prismaMock.moderationAction.create.mockResolvedValueOnce({}); await banUserFromModerationItem("comment", "reply-1", null, null); expect(prismaMock.profile.update).toHaveBeenCalledWith({ where: { id: "user-99" }, data: expect.objectContaining({ banned: true, bannedReason: "Moderation: comment reply-1", }), }); }); it("404 when target item not found", async () => { prismaMock.communityPost.findUnique.mockResolvedValueOnce(null); await expect( banUserFromModerationItem("post", "nonexistent", null), ).rejects.toMatchObject({ statusCode: 404 }); expect(prismaMock.profile.update).not.toHaveBeenCalled(); }); });