import { revalidatePath } from "next/cache"; import { beforeEach, describe, expect, it, vi } from "vitest"; const { execute, registrations, staff } = vi.hoisted(() => ({ execute: vi.fn(), registrations: [] as Array<{ permission: string | readonly string[] }>, staff: { id: 42, rank: 4, username: "moderator" }, })); function wrapper( options: { permission: string | readonly string[]; schema?: { safeParse( value: unknown, ): { success: true; data: unknown } | { success: false; error: unknown }; }; }, handler: (context: { data: unknown; session: { user: typeof staff }; }) => unknown, ) { registrations.push(options); return async (data: unknown) => { const parsed = options.schema?.safeParse(data); if (parsed && !parsed.success) { return { ok: false, error: "Validation failed" }; } try { return await handler({ data: parsed?.data ?? data, session: { user: staff }, }); } catch (error) { return { ok: false, error: error instanceof Error ? error.message : "Internal server error", }; } }; } vi.mock("@/features/housekeeping/domains/people/services/mutations", () => ({ createPeopleMutationInvocation: vi.fn((actor, correlationId) => ({ expectedActorId: actor.id, correlationId, legacy: true, })), peopleMutationService: { execute }, })); vi.mock("@/lib/safe-action", () => ({ adminAction: wrapper })); vi.mock("@/lib/safe-action-shared", () => ({ ActionError: class ActionError extends Error {}, actionOk: (data?: unknown) => ({ ok: true, data: data ?? {} }), })); vi.mock("@/lib/foundation/action", () => ({ adminAction: wrapper, actionOk: (data?: unknown) => ({ ok: true, data: data ?? {} }), })); vi.mock("@/lib/permissions", () => ({ PERMS: { TICKETS_EDIT: "admin.tickets.edit", MOD_TICKETS_EDIT: "mod.tickets.edit", USERS_BAN: "admin.users.ban", MODERATION_EDIT: "admin.moderation.edit", MOD_CFH_EDIT: "mod.cfh.edit", MOD_ACTIONS: "mod.actions", }, })); vi.mock("next/cache", () => ({ revalidatePath: vi.fn() })); import { closeHelpCenterTicket, liftBanFromHelpTicket, reopenHelpCenterTicket, replyHelpCenterTicket, } from "./admin-help-tickets"; import { assignCfhTicket, broadcastAlert, closeCfhTicket, quickAlert, quickKick, quickMute, quickRoomKick, quickUnmute, updateCfhState, } from "./moderation"; import { createTemplate, deleteTemplate, updateTemplate, } from "./ticket-templates"; import { adminReplyTicket, assignTicket, updateTicketPriority, updateTicketStatus, } from "./tickets"; type LegacyAction = (input: unknown) => Promise; const call = (action: unknown, input: unknown) => (action as LegacyAction)(input); beforeEach(() => { vi.clearAllMocks(); execute.mockImplementation(async (invocation, operation) => ({ ok: true, data: { before: null, after: operation === "ticket-template.change" ? { id: "88" } : {}, output: operation === "help-ticket.unban" ? { removed: 2, userId: 7 } : undefined, }, correlationId: invocation.correlationId, })); }); describe("legacy People support and moderation wrappers", () => { it("keeps mid-rank ACL alternatives without an admin.dashboard dependency", () => { const permissions = registrations.flatMap((entry) => typeof entry.permission === "string" ? [entry.permission] : entry.permission, ); expect(permissions).toEqual( expect.arrayContaining([ "admin.tickets.edit", "mod.tickets.edit", "admin.moderation.edit", "mod.cfh.edit", "mod.actions", ]), ); expect(permissions).not.toContain("admin.dashboard"); }); it("delegates tickets and templates with their established result shapes", async () => { await expect( call(adminReplyTicket, { ticketId: 7, message: "Handled" }), ).resolves.toEqual({ ok: true, data: {} }); await call(assignTicket, { ticketId: 7, assigneeId: 42 }); await call(updateTicketStatus, { ticketId: 7, status: "closed" }); await call(updateTicketPriority, { ticketId: 7, priority: "urgent" }); await expect( call(createTemplate, { title: "Greeting", content: "Hello", category: "general", sortOrder: 0, }), ).resolves.toEqual({ ok: true, data: { id: 88 } }); await expect( call(updateTemplate, { id: 88, title: "Updated" }), ).resolves.toEqual({ ok: true, data: { id: 88 } }); await expect(call(deleteTemplate, { id: 88 })).resolves.toEqual({ ok: true, data: {}, }); expect(execute.mock.calls.map((entry) => entry[1])).toEqual([ "ticket.reply", "ticket.assign", "ticket.status", "ticket.priority", "ticket-template.change", "ticket-template.change", "ticket-template.change", ]); }); it("preserves BIGINT help-ticket IDs, outputs, and every legacy refresh", async () => { const ticketId = 9_007_199_254_740_993n; await call(replyHelpCenterTicket, { ticketId, content: " Handled " }); await call(closeHelpCenterTicket, { ticketId }); await call(reopenHelpCenterTicket, { ticketId }); await expect(call(liftBanFromHelpTicket, { ticketId })).resolves.toEqual({ ok: true, data: { removed: 2, userId: 7 }, }); expect(execute.mock.calls.map((entry) => entry[2])).toEqual([ { ticketId: "9007199254740993", content: "Handled" }, { ticketId: "9007199254740993", status: "close" }, { ticketId: "9007199254740993", status: "reopen" }, { ticketId: "9007199254740993" }, ]); expect(revalidatePath).toHaveBeenCalledWith( "/ase/people/support/help-tickets", ); expect(revalidatePath).toHaveBeenCalledWith( "/ase/people/support/help-tickets/9007199254740993", ); expect(revalidatePath).toHaveBeenCalledWith( "/help/tickets/9007199254740993", ); expect(revalidatePath).toHaveBeenCalledWith("/ase/people/moderation/bans"); expect(revalidatePath).toHaveBeenCalledWith("/ase/people/users/7"); }); it("delegates every CFH and moderation transport action", async () => { await call(assignCfhTicket, { ticketId: 9 }); await call(updateCfhState, { ticketId: 9, state: 3 }); await call(closeCfhTicket, { ticketId: 9 }); await call(quickKick, { userId: 7 }); await call(quickMute, { userId: 7, duration: 60 }); await call(quickUnmute, { userId: 7 }); await call(quickAlert, { userId: 7, message: "Stop" }); await call(quickRoomKick, { roomId: 12 }); await call(broadcastAlert, { message: "Notice", type: "staff" }); expect(execute.mock.calls.map((entry) => entry[1])).toEqual([ "cfh.resolve", "cfh.resolve", "cfh.resolve", "moderation.action", "moderation.action", "moderation.action", "moderation.action", "moderation.action", "moderation.action", ]); expect(execute.mock.calls.map((entry) => entry[0].expectedActorId)).toEqual( Array(9).fill(42), ); }); it.each([ ["kick", quickKick, { userId: 7 }], ["mute", quickMute, { userId: 7, duration: 60 }], ["unmute", quickUnmute, { userId: 7 }], ["alert", quickAlert, { userId: 7, message: "Stop" }], ["room kick", quickRoomKick, { roomId: 12 }], ["broadcast", broadcastAlert, { message: "Notice", type: "staff" }], ] as const)( "maps a non-ok %s service result to the historical legacy failure boundary", async (_label, action, input) => { execute.mockResolvedValueOnce({ ok: false, error: { code: "DEPENDENCY_UNAVAILABLE", messageKey: "errors.housekeeping.dependencyUnavailable", }, correlationId: "quick-action-failure", }); await expect(call(action, input)).resolves.toEqual({ ok: false, error: "Could not execute moderation action", }); }, ); it("maps a thrown moderation service failure instead of reporting success", async () => { execute.mockRejectedValueOnce(new Error("RCON unavailable")); await expect(call(quickKick, { userId: 7 })).resolves.toEqual({ ok: false, error: "RCON unavailable", }); }); it.each([ 9_007_199_254_740_992, "01", "0", 0, -1, "18446744073709551616", ] as const)( "rejects noncanonical help-ticket identifier %s at every legacy action schema", async (ticketId) => { for (const [action, input] of [ [replyHelpCenterTicket, { ticketId, content: "Handled" }], [closeHelpCenterTicket, { ticketId }], [reopenHelpCenterTicket, { ticketId }], [liftBanFromHelpTicket, { ticketId }], ] as const) { await expect(call(action, input)).resolves.toEqual({ ok: false, error: "Validation failed", }); } expect(execute).not.toHaveBeenCalled(); }, ); it("keeps close-CFH missing rows as a successful legacy no-op", async () => { execute.mockResolvedValueOnce({ ok: false, error: { code: "NOT_FOUND", messageKey: "errors.housekeeping.notFound" }, correlationId: "missing-cfh", }); await expect(call(closeCfhTicket, { ticketId: 404 })).resolves.toEqual({ ok: true, data: {}, }); }); });