import { describe, it, expect, mock, beforeEach } from "bun:test"; import { ModerationService } from "@shared/modules/moderation/moderation.service"; import { moderationCases } from "@db/schema"; import { CaseType } from "@shared/lib/constants"; // Mock Drizzle Functions const mockFindFirst = mock(); const mockFindMany = mock(); const mockInsert = mock(); const mockUpdate = mock(); const mockValues = mock(); const mockReturning = mock(); const mockSet = mock(); const mockWhere = mock(); // Mock Config const mockConfig = { moderation: { cases: { dmOnWarn: true, autoTimeoutThreshold: 3 } } }; mock.module("@shared/lib/config", () => ({ config: mockConfig })); // Mock View const mockGetUserWarningEmbed = mock(() => ({})); mock.module("./moderation.view", () => ({ getUserWarningEmbed: mockGetUserWarningEmbed })); // Mock DrizzleClient mock.module("@shared/db/DrizzleClient", () => ({ DrizzleClient: { query: { moderationCases: { findFirst: mockFindFirst, findMany: mockFindMany, }, }, insert: mockInsert, update: mockUpdate, } })); // Setup chains mockInsert.mockReturnValue({ values: mockValues }); mockValues.mockReturnValue({ returning: mockReturning }); mockUpdate.mockReturnValue({ set: mockSet }); mockSet.mockReturnValue({ where: mockWhere }); mockWhere.mockReturnValue({ returning: mockReturning }); describe("ModerationService", () => { beforeEach(() => { mockFindFirst.mockReset(); mockFindMany.mockReset(); mockInsert.mockClear(); mockUpdate.mockClear(); mockValues.mockClear(); mockReturning.mockClear(); mockSet.mockClear(); mockWhere.mockClear(); mockGetUserWarningEmbed.mockClear(); // Reset config to defaults mockConfig.moderation.cases.dmOnWarn = true; mockConfig.moderation.cases.autoTimeoutThreshold = 3; }); describe("issueWarning", () => { const defaultOptions = { userId: "123456789", username: "testuser", moderatorId: "987654321", moderatorName: "mod", reason: "test reason", guildName: "Test Guild" }; it("should issue a warning and attempt to DM the user", async () => { mockFindFirst.mockResolvedValue({ caseId: "CASE-0001" }); mockReturning.mockResolvedValue([{ caseId: "CASE-0002" }]); mockFindMany.mockResolvedValue([{ type: CaseType.WARN, active: true }]); // 1 warning total const mockDmTarget = { send: mock() }; const result = await ModerationService.issueWarning({ ...defaultOptions, dmTarget: mockDmTarget }); expect(result.moderationCase).toBeDefined(); expect(result.warningCount).toBe(1); expect(mockDmTarget.send).toHaveBeenCalled(); expect(mockGetUserWarningEmbed).toHaveBeenCalled(); }); it("should not DM if dmOnWarn is false", async () => { mockConfig.moderation.cases.dmOnWarn = false; mockFindFirst.mockResolvedValue({ caseId: "CASE-0001" }); mockReturning.mockResolvedValue([{ caseId: "CASE-0002" }]); mockFindMany.mockResolvedValue([]); const mockDmTarget = { send: mock() }; await ModerationService.issueWarning({ ...defaultOptions, dmTarget: mockDmTarget }); expect(mockDmTarget.send).not.toHaveBeenCalled(); }); it("should trigger auto-timeout when threshold is reached", async () => { mockFindFirst.mockResolvedValue({ caseId: "CASE-0001" }); mockReturning.mockResolvedValue([{ caseId: "CASE-0002" }]); // Simulate 3 warnings (threshold is 3) mockFindMany.mockResolvedValue([{}, {}, {}]); const mockTimeoutTarget = { timeout: mock() }; const result = await ModerationService.issueWarning({ ...defaultOptions, timeoutTarget: mockTimeoutTarget }); expect(result.autoTimeoutIssued).toBe(true); expect(mockTimeoutTarget.timeout).toHaveBeenCalledWith(86400000, expect.stringContaining("3 warnings")); // Should create two cases: one for warn, one for timeout expect(mockInsert).toHaveBeenCalledTimes(2); }); it("should not timeout if threshold is not reached", async () => { mockFindFirst.mockResolvedValue({ caseId: "CASE-0001" }); mockReturning.mockResolvedValue([{ caseId: "CASE-0002" }]); // Simulate 2 warnings (threshold is 3) mockFindMany.mockResolvedValue([{}, {}]); const mockTimeoutTarget = { timeout: mock() }; const result = await ModerationService.issueWarning({ ...defaultOptions, timeoutTarget: mockTimeoutTarget }); expect(result.autoTimeoutIssued).toBe(false); expect(mockTimeoutTarget.timeout).not.toHaveBeenCalled(); expect(mockInsert).toHaveBeenCalledTimes(1); }); }); describe("getNextCaseId", () => { it("should return CASE-0001 if no cases exist", async () => { mockFindFirst.mockResolvedValue(undefined); // Accessing private method via bracket notation for testing const nextId = await (ModerationService as any).getNextCaseId(); expect(nextId).toBe("CASE-0001"); }); it("should increment the latest case ID", async () => { mockFindFirst.mockResolvedValue({ caseId: "CASE-0042" }); const nextId = await (ModerationService as any).getNextCaseId(); expect(nextId).toBe("CASE-0043"); }); it("should handle padding correctly (e.g., 9 -> 0010)", async () => { mockFindFirst.mockResolvedValue({ caseId: "CASE-0009" }); const nextId = await (ModerationService as any).getNextCaseId(); expect(nextId).toBe("CASE-0010"); }); }); describe("createCase", () => { it("should create a new moderation case with correct values", async () => { mockFindFirst.mockResolvedValue({ caseId: "CASE-0001" }); const mockNewCase = { caseId: "CASE-0002", type: CaseType.WARN, userId: 123456789n, username: "testuser", moderatorId: 987654321n, moderatorName: "mod", reason: "test reason", metadata: {}, active: true }; mockReturning.mockResolvedValue([mockNewCase]); const result = await ModerationService.createCase({ type: CaseType.WARN, userId: "123456789", username: "testuser", moderatorId: "987654321", moderatorName: "mod", reason: "test reason" }); expect(result?.caseId).toBe("CASE-0002"); expect(mockInsert).toHaveBeenCalled(); expect(mockValues).toHaveBeenCalledWith(expect.objectContaining({ caseId: "CASE-0002", type: CaseType.WARN, userId: 123456789n, reason: "test reason" })); }); it("should set active to false for non-warn types", async () => { mockFindFirst.mockResolvedValue(undefined); mockReturning.mockImplementation((values) => [values]); // Simplified mock const result = await ModerationService.createCase({ type: CaseType.BAN, userId: "123456789", username: "testuser", moderatorId: "987654321", moderatorName: "mod", reason: "test reason" }); expect(mockValues).toHaveBeenCalledWith(expect.objectContaining({ active: false })); }); }); describe("getCaseById", () => { it("should return a case by its ID", async () => { const mockCase = { caseId: "CASE-0001", reason: "test" }; mockFindFirst.mockResolvedValue(mockCase); const result = await ModerationService.getCaseById("CASE-0001"); expect(result).toEqual(mockCase as any); }); }); describe("getUserCases", () => { it("should return all cases for a user", async () => { const mockCases = [{ caseId: "CASE-0001" }, { caseId: "CASE-0002" }]; mockFindMany.mockResolvedValue(mockCases); const result = await ModerationService.getUserCases("123456789"); expect(result).toHaveLength(2); expect(mockFindMany).toHaveBeenCalled(); }); }); describe("clearCase", () => { it("should update a case to be inactive and resolved", async () => { const mockUpdatedCase = { caseId: "CASE-0001", active: false }; mockReturning.mockResolvedValue([mockUpdatedCase]); const result = await ModerationService.clearCase({ caseId: "CASE-0001", clearedBy: "987654321", clearedByName: "mod", reason: "resolved" }); expect(result?.active).toBe(false); expect(mockUpdate).toHaveBeenCalled(); expect(mockSet).toHaveBeenCalledWith(expect.objectContaining({ active: false, resolvedBy: 987654321n, resolvedReason: "resolved" })); }); }); describe("getActiveWarningCount", () => { it("should return the number of active warnings", async () => { mockFindMany.mockResolvedValue([ { id: 1n, type: CaseType.WARN, active: true }, { id: 2n, type: CaseType.WARN, active: true } ]); const count = await ModerationService.getActiveWarningCount("123456789"); expect(count).toBe(2); }); it("should return 0 if no active warnings", async () => { mockFindMany.mockResolvedValue([]); const count = await ModerationService.getActiveWarningCount("123456789"); expect(count).toBe(0); }); }); });