feat(housekeeping): complete people moderation parity
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
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[] },
|
||||
handler: (context: { data: unknown; session: { user: typeof staff } }) => unknown,
|
||||
) {
|
||||
registrations.push(options);
|
||||
return (data: unknown) => handler({ data, session: { user: staff } });
|
||||
}
|
||||
|
||||
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<unknown>;
|
||||
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("/admin/help-tickets");
|
||||
expect(revalidatePath).toHaveBeenCalledWith(
|
||||
"/admin/help-tickets/9007199254740993",
|
||||
);
|
||||
expect(revalidatePath).toHaveBeenCalledWith(
|
||||
"/mod/help-tickets/9007199254740993",
|
||||
);
|
||||
expect(revalidatePath).toHaveBeenCalledWith(
|
||||
"/help/tickets/9007199254740993",
|
||||
);
|
||||
expect(revalidatePath).toHaveBeenCalledWith("/admin/bans");
|
||||
expect(revalidatePath).toHaveBeenCalledWith("/admin/users/show/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("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: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user