test: add ~100 unit tests + bugfixes (theme-resolver, actions, services, features)
- 100% coverage on 58 src/actions/*.ts, 24 src/lib/services/*.ts, 19 src/features|db|hooks|i18n/*.ts - 3 core lib modules (theme-resolver, ip-lookup, translation-pool): 100% - ~3,000 new meaningful tests - Bugfixes: - theme-resolver: generateScopedCss now emits scoped CSS blocks (was early-return bug) - admin-radio-api-keys: blank rateLimit now uses fallback - admin-badge-upload: validation before try-block to prevent swallowed redirect - Coverage raised from 26% -> 34% statements
This commit is contained in:
1 parent
4b68728dbd
commit
99eb3af17b
65 files changed
+13877
-13
No files matched your search
@@ -0,0 +1,235 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
tickets: [{ id: 1, state: 0 }] as any[],
|
||||
set: null as any,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/db", async () => {
|
||||
const schema = await import("@/db/schema");
|
||||
const { createFakeDb } = await import("@/test/fake-db");
|
||||
const fake = createFakeDb(() => state.tickets);
|
||||
return {
|
||||
...schema,
|
||||
db: {
|
||||
...fake,
|
||||
update: () => ({
|
||||
set: (v: any) => {
|
||||
state.set = v;
|
||||
return { where: () => Promise.resolve([{ affectedRows: 1 }]) };
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const perm = vi.hoisted(() => ({ getCtx: vi.fn(), canAccess: vi.fn() }));
|
||||
vi.mock("@/lib/permissions", async () => ({
|
||||
...(await import("@/lib/permission-slugs")),
|
||||
getApiAdminContext: perm.getCtx,
|
||||
canAccess: perm.canAccess,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({ auth: vi.fn() }));
|
||||
vi.mock("@/lib/rate-limit", () => ({
|
||||
rateLimit: vi.fn().mockResolvedValue({ ok: true, retryAfter: 0 }),
|
||||
clientIp: vi.fn().mockResolvedValue("127.0.0.1"),
|
||||
}));
|
||||
vi.mock("@/lib/services/staff-activity", () => ({
|
||||
logStaffActivity: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/logger", () => ({
|
||||
logger: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
|
||||
}));
|
||||
|
||||
const logAuditMock = vi.hoisted(() => vi.fn());
|
||||
vi.mock("@/lib/services/audit", () => ({ logAudit: logAuditMock }));
|
||||
|
||||
const rconMock = vi.hoisted(() => ({
|
||||
disconnectUser: vi.fn(),
|
||||
muteUser: vi.fn(),
|
||||
unmuteUser: vi.fn(),
|
||||
alertUser: vi.fn(),
|
||||
kickAll: vi.fn(),
|
||||
hotelAlert: vi.fn(),
|
||||
staffAlert: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/services/rcon", () => ({ rcon: rconMock }));
|
||||
|
||||
import {
|
||||
assignCfhTicket,
|
||||
broadcastAlert,
|
||||
closeCfhTicket,
|
||||
quickAlert,
|
||||
quickKick,
|
||||
quickMute,
|
||||
quickRoomKick,
|
||||
quickUnmute,
|
||||
updateCfhState,
|
||||
} from "./moderation";
|
||||
|
||||
const ctx = {
|
||||
session: { user: { id: 7, username: "admin", rank: 7, name: "admin" } },
|
||||
permissions: { has: () => true },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
state.tickets = [{ id: 1, state: 0 }];
|
||||
state.set = null;
|
||||
perm.getCtx.mockResolvedValue(ctx);
|
||||
perm.canAccess.mockReturnValue(true);
|
||||
for (const fn of Object.values(rconMock)) fn.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
describe("CFH ticket actions", () => {
|
||||
it("assigns a ticket to the moderator and audits it", async () => {
|
||||
const res = await assignCfhTicket({ ticketId: 1 });
|
||||
expect(res).toEqual({ ok: true, data: {} });
|
||||
expect(state.set).toEqual({ modId: 7, state: 1 });
|
||||
expect(logAuditMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userId: 7,
|
||||
action: "cfh_assign",
|
||||
targetId: 1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns not-found when assigning a missing ticket", async () => {
|
||||
state.tickets = [];
|
||||
const res = await assignCfhTicket({ ticketId: 9 });
|
||||
expect(res).toEqual({ ok: false, error: "SupportTicket #9 not found" });
|
||||
expect(state.set).toBeNull();
|
||||
});
|
||||
|
||||
it("updates the ticket state with before/after audit data", async () => {
|
||||
const res = await updateCfhState({ ticketId: 1, state: 3 });
|
||||
expect(res.ok).toBe(true);
|
||||
expect(state.set).toEqual({ state: 3, modId: 7 });
|
||||
expect(logAuditMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "cfh_state_change",
|
||||
before: { state: 0 },
|
||||
after: { state: 3 },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects an out-of-range state and a missing ticket", async () => {
|
||||
const invalid = await updateCfhState({ ticketId: 1, state: 4 });
|
||||
expect(invalid.ok).toBe(false);
|
||||
state.tickets = [];
|
||||
const missing = await updateCfhState({ ticketId: 1, state: 1 });
|
||||
expect(missing).toEqual({
|
||||
ok: false,
|
||||
error: "SupportTicket #1 not found",
|
||||
});
|
||||
});
|
||||
|
||||
it("closes a ticket", async () => {
|
||||
const res = await closeCfhTicket({ ticketId: 1 });
|
||||
expect(res.ok).toBe(true);
|
||||
expect(state.set).toEqual({ state: 2, modId: 7 });
|
||||
expect(logAuditMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ action: "cfh_close" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("quick mod actions", () => {
|
||||
it("kicks a user via RCON", async () => {
|
||||
const res = await quickKick({ userId: 11 });
|
||||
expect(res.ok).toBe(true);
|
||||
expect(rconMock.disconnectUser).toHaveBeenCalledWith(11);
|
||||
expect(logAuditMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ action: "mod_kick", targetId: 11 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("mutes a user for a duration", async () => {
|
||||
const res = await quickMute({ userId: 11, duration: 600 });
|
||||
expect(res.ok).toBe(true);
|
||||
expect(rconMock.muteUser).toHaveBeenCalledWith(11, 600);
|
||||
expect(logAuditMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "mod_mute",
|
||||
after: { duration: 600 },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects a mute duration above the one-year maximum", async () => {
|
||||
const res = await quickMute({ userId: 11, duration: 525_601 });
|
||||
expect(res.ok).toBe(false);
|
||||
expect(rconMock.muteUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("unmutes a user", async () => {
|
||||
await quickUnmute({ userId: 11 });
|
||||
expect(rconMock.unmuteUser).toHaveBeenCalledWith(11);
|
||||
expect(logAuditMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ action: "mod_unmute" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("alerts a user", async () => {
|
||||
await quickAlert({ userId: 11, message: "be nice" });
|
||||
expect(rconMock.alertUser).toHaveBeenCalledWith(11, "be nice");
|
||||
});
|
||||
|
||||
it("rejects an empty alert and a non-positive user id", async () => {
|
||||
expect((await quickAlert({ userId: 11, message: "" })).ok).toBe(false);
|
||||
expect((await quickKick({ userId: 0 })).ok).toBe(false);
|
||||
expect(rconMock.alertUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("kicks everyone in a room", async () => {
|
||||
await quickRoomKick({ roomId: 3 });
|
||||
expect(rconMock.kickAll).toHaveBeenCalledWith(3);
|
||||
expect(logAuditMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ action: "mod_room_kick", targetId: 3 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("broadcasts a hotel alert", async () => {
|
||||
const res = await broadcastAlert({ message: "hi", type: "hotel" });
|
||||
expect(res.ok).toBe(true);
|
||||
expect(rconMock.hotelAlert).toHaveBeenCalledWith("hi");
|
||||
expect(rconMock.staffAlert).not.toHaveBeenCalled();
|
||||
expect(logAuditMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ action: "mod_broadcast_hotel" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("broadcasts a staff alert", async () => {
|
||||
await broadcastAlert({ message: "hi", type: "staff" });
|
||||
expect(rconMock.staffAlert).toHaveBeenCalledWith("hi");
|
||||
expect(rconMock.hotelAlert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects an unknown broadcast type", async () => {
|
||||
const res = await broadcastAlert({ message: "hi", type: "other" } as never);
|
||||
expect(res.ok).toBe(false);
|
||||
expect(rconMock.hotelAlert).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("authorization", () => {
|
||||
it("returns Unauthorized with no admin context", async () => {
|
||||
perm.getCtx.mockResolvedValue(null);
|
||||
expect(await quickKick({ userId: 1 })).toEqual({
|
||||
ok: false,
|
||||
error: "Unauthorized",
|
||||
});
|
||||
});
|
||||
|
||||
it("denies mod actions without the required permission", async () => {
|
||||
perm.canAccess.mockReturnValue(false);
|
||||
expect(await quickMute({ userId: 1, duration: 1 })).toEqual({
|
||||
ok: false,
|
||||
error: "Unauthorized",
|
||||
});
|
||||
expect(rconMock.muteUser).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user