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,357 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
canAccess: vi.fn(() => true),
|
||||
getApiAdminContext: vi.fn(),
|
||||
logAuthorizationEvent: vi.fn(),
|
||||
logAudit: vi.fn(),
|
||||
revalidatePath: vi.fn(),
|
||||
extractClientIpAsync: vi.fn(async () => "127.0.0.1"),
|
||||
rateLimit: vi.fn(async () => ({ ok: true })),
|
||||
reportError: vi.fn(),
|
||||
selectRows: [] as unknown[],
|
||||
updateSet: vi.fn(),
|
||||
updateWhere: vi.fn(async () => [{ affectedRows: 1 }]),
|
||||
insertValues: vi.fn(async () => [{}]),
|
||||
deleteWhere: vi.fn(async () => [{ affectedRows: 2 }]),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/admin/authorization-events", () => ({
|
||||
logAuthorizationEvent: mocks.logAuthorizationEvent,
|
||||
}));
|
||||
vi.mock("@/lib/auth", () => ({ auth: vi.fn() }));
|
||||
vi.mock("@/lib/permissions", () => ({
|
||||
PERMS: {
|
||||
USERS_BAN: "admin.users.ban",
|
||||
TICKETS_EDIT: "admin.tickets.edit",
|
||||
MOD_TICKETS_EDIT: "mod.tickets.edit",
|
||||
},
|
||||
canAccess: mocks.canAccess,
|
||||
getApiAdminContext: mocks.getApiAdminContext,
|
||||
}));
|
||||
vi.mock("@/lib/rate-limit", () => ({ rateLimit: mocks.rateLimit }));
|
||||
vi.mock("@/lib/report-error", () => ({ reportError: mocks.reportError }));
|
||||
vi.mock("@/lib/foundation/security", () => ({
|
||||
extractClientIpAsync: mocks.extractClientIpAsync,
|
||||
}));
|
||||
vi.mock("@/lib/services/audit", () => ({ logAudit: mocks.logAudit }));
|
||||
vi.mock("next/cache", () => ({ revalidatePath: mocks.revalidatePath }));
|
||||
vi.mock("@/lib/db", async () => {
|
||||
const schema = await import("@/db/schema");
|
||||
const { createFakeDb } = await import("@/test/fake-db");
|
||||
const fake = createFakeDb(() => mocks.selectRows);
|
||||
return {
|
||||
...schema,
|
||||
db: {
|
||||
...fake,
|
||||
update: vi.fn(() => ({ set: mocks.updateSet })),
|
||||
delete: vi.fn(() => ({ where: mocks.deleteWhere })),
|
||||
transaction: vi.fn(async (fn: (tx: unknown) => unknown) =>
|
||||
fn({
|
||||
insert: vi.fn(() => ({ values: mocks.insertValues })),
|
||||
update: vi.fn(() => ({ set: mocks.updateSet })),
|
||||
}),
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import {
|
||||
closeHelpCenterTicket,
|
||||
liftBanFromHelpTicket,
|
||||
reopenHelpCenterTicket,
|
||||
replyHelpCenterTicket,
|
||||
} from "./admin-help-tickets";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.canAccess.mockReturnValue(true);
|
||||
mocks.getApiAdminContext.mockResolvedValue({
|
||||
session: { user: { id: 7, rank: 7, name: "staff" } },
|
||||
permissions: {},
|
||||
});
|
||||
mocks.selectRows = [];
|
||||
mocks.updateSet.mockReturnValue({ where: mocks.updateWhere });
|
||||
mocks.updateWhere.mockResolvedValue([{ affectedRows: 1 }]);
|
||||
mocks.insertValues.mockResolvedValue([{}]);
|
||||
mocks.deleteWhere.mockResolvedValue([{ affectedRows: 2 }]);
|
||||
});
|
||||
|
||||
function ticketId(id: string | number | bigint) {
|
||||
return { ticketId: id };
|
||||
}
|
||||
|
||||
describe("liftBanFromHelpTicket", () => {
|
||||
it("removes bans, reopens state and returns the removed count", async () => {
|
||||
mocks.selectRows = [{ id: 5, userId: 3, open: true, title: "Appeal" }];
|
||||
|
||||
const result = await liftBanFromHelpTicket(ticketId("5"));
|
||||
|
||||
expect(result).toEqual({ ok: true, data: { removed: 2, userId: 3 } });
|
||||
expect(mocks.logAudit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userId: 7,
|
||||
action: "unban_via_help_ticket",
|
||||
target: "User",
|
||||
targetId: 3,
|
||||
after: { ticketId: "5", removedBans: 2, title: "Appeal" },
|
||||
}),
|
||||
);
|
||||
expect(mocks.updateSet).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ open: false, updatedAt: expect.any(Date) }),
|
||||
);
|
||||
expect(mocks.revalidatePath).toHaveBeenCalledWith("/admin/help-tickets");
|
||||
expect(mocks.revalidatePath).toHaveBeenCalledWith("/admin/bans");
|
||||
expect(mocks.revalidatePath).toHaveBeenCalledWith("/admin/users/show/3");
|
||||
});
|
||||
|
||||
it("skips closing an already closed ticket", async () => {
|
||||
mocks.selectRows = [{ id: 5, userId: 3, open: false, title: "Appeal" }];
|
||||
|
||||
await liftBanFromHelpTicket(ticketId("5"));
|
||||
|
||||
expect(mocks.updateSet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports a missing ticket", async () => {
|
||||
mocks.selectRows = [];
|
||||
|
||||
expect(await liftBanFromHelpTicket(ticketId("5"))).toEqual({
|
||||
ok: false,
|
||||
error: "Ticket not found",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports a ticket without a requester", async () => {
|
||||
mocks.selectRows = [{ id: 5, userId: null, open: true, title: "x" }];
|
||||
|
||||
expect(await liftBanFromHelpTicket(ticketId("5"))).toEqual({
|
||||
ok: false,
|
||||
error: "Ticket has no requester to unban",
|
||||
});
|
||||
expect(mocks.deleteWhere).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("defaults the removed count to zero when the driver omits affectedRows", async () => {
|
||||
mocks.selectRows = [{ id: 5, userId: 3, open: true, title: "x" }];
|
||||
mocks.deleteWhere.mockResolvedValueOnce([{}]);
|
||||
|
||||
expect(await liftBanFromHelpTicket(ticketId("5"))).toEqual({
|
||||
ok: true,
|
||||
data: { removed: 0, userId: 3 },
|
||||
});
|
||||
});
|
||||
|
||||
it("transforms numeric and bigint ticket ids", async () => {
|
||||
mocks.selectRows = [{ id: 5, userId: 3, open: true, title: "x" }];
|
||||
|
||||
await liftBanFromHelpTicket(ticketId(5));
|
||||
await liftBanFromHelpTicket(ticketId(5n));
|
||||
|
||||
expect(mocks.selectRows).toBeDefined();
|
||||
});
|
||||
|
||||
it("rejects unauthenticated callers", async () => {
|
||||
mocks.getApiAdminContext.mockResolvedValueOnce(null);
|
||||
|
||||
expect(await liftBanFromHelpTicket(ticketId("5"))).toEqual({
|
||||
ok: false,
|
||||
error: "Unauthorized",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects callers without the ban permission and logs an authorization event", async () => {
|
||||
mocks.canAccess.mockReturnValue(false);
|
||||
|
||||
expect(await liftBanFromHelpTicket(ticketId("5"))).toEqual({
|
||||
ok: false,
|
||||
error: "Unauthorized",
|
||||
});
|
||||
expect(mocks.logAuthorizationEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
kind: "permission.denied",
|
||||
userId: 7,
|
||||
permission: PERMS.USERS_BAN,
|
||||
source: "adminAction",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects malformed input before the handler runs", async () => {
|
||||
const result = await liftBanFromHelpTicket({ ticketId: true } as never);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result).toMatchObject({ error: "Validation failed" });
|
||||
expect(mocks.logAudit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("replyHelpCenterTicket", () => {
|
||||
it("inserts a trimmed reply and bumps the ticket inside a transaction", async () => {
|
||||
mocks.selectRows = [{ id: 5, open: true }];
|
||||
|
||||
const result = await replyHelpCenterTicket({
|
||||
ticketId: "5",
|
||||
content: " thanks ",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true, data: {} });
|
||||
expect(mocks.insertValues).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ticketId: 5n,
|
||||
userId: 7,
|
||||
content: "thanks",
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date),
|
||||
}),
|
||||
);
|
||||
expect(mocks.updateSet).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ updatedAt: expect.any(Date) }),
|
||||
);
|
||||
expect(mocks.logAudit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "help_center_ticket_reply",
|
||||
target: "WebsiteHelpCenterTickets",
|
||||
targetId: 5,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("allows a moderator permission from the any-of list", async () => {
|
||||
mocks.selectRows = [{ id: 5, open: true }];
|
||||
mocks.canAccess.mockImplementation(
|
||||
(_perms: unknown, slug: string) => slug === PERMS.MOD_TICKETS_EDIT,
|
||||
);
|
||||
|
||||
const result = await replyHelpCenterTicket({
|
||||
ticketId: "5",
|
||||
content: "ok",
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(mocks.canAccess).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
PERMS.TICKETS_EDIT,
|
||||
expect.anything(),
|
||||
);
|
||||
expect(mocks.canAccess).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
PERMS.MOD_TICKETS_EDIT,
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports a missing ticket", async () => {
|
||||
mocks.selectRows = [];
|
||||
|
||||
expect(
|
||||
await replyHelpCenterTicket({ ticketId: "5", content: "hi" }),
|
||||
).toEqual({ ok: false, error: "Ticket not found" });
|
||||
});
|
||||
|
||||
it("rejects an empty reply", async () => {
|
||||
mocks.selectRows = [{ id: 5, open: true }];
|
||||
|
||||
const result = await replyHelpCenterTicket({ ticketId: "5", content: "" });
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result).toMatchObject({ error: "Validation failed" });
|
||||
expect(mocks.insertValues).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects unauthenticated callers", async () => {
|
||||
mocks.getApiAdminContext.mockResolvedValueOnce(null);
|
||||
|
||||
expect(
|
||||
await replyHelpCenterTicket({ ticketId: "5", content: "hi" }),
|
||||
).toEqual({ ok: false, error: "Unauthorized" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("closeHelpCenterTicket", () => {
|
||||
it("closes an open ticket and records the audit trail", async () => {
|
||||
mocks.selectRows = [{ id: 5, open: true }];
|
||||
|
||||
const result = await closeHelpCenterTicket(ticketId("5"));
|
||||
|
||||
expect(result).toEqual({ ok: true, data: {} });
|
||||
expect(mocks.updateSet).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ open: false, updatedAt: expect.any(Date) }),
|
||||
);
|
||||
expect(mocks.logAudit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "help_center_ticket_close",
|
||||
before: { open: true },
|
||||
after: { open: false },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports a missing ticket", async () => {
|
||||
mocks.selectRows = [];
|
||||
|
||||
expect(await closeHelpCenterTicket(ticketId("5"))).toEqual({
|
||||
ok: false,
|
||||
error: "Ticket not found",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports an already closed ticket", async () => {
|
||||
mocks.selectRows = [{ id: 5, open: false }];
|
||||
|
||||
expect(await closeHelpCenterTicket(ticketId("5"))).toEqual({
|
||||
ok: false,
|
||||
error: "Ticket is already closed",
|
||||
});
|
||||
expect(mocks.updateSet).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("reopenHelpCenterTicket", () => {
|
||||
it("reopens a closed ticket and records the audit trail", async () => {
|
||||
mocks.selectRows = [{ id: 5, open: false }];
|
||||
|
||||
const result = await reopenHelpCenterTicket(ticketId("5"));
|
||||
|
||||
expect(result).toEqual({ ok: true, data: {} });
|
||||
expect(mocks.updateSet).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ open: true, updatedAt: expect.any(Date) }),
|
||||
);
|
||||
expect(mocks.logAudit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "help_center_ticket_reopen",
|
||||
before: { open: false },
|
||||
after: { open: true },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports a missing ticket", async () => {
|
||||
mocks.selectRows = [];
|
||||
|
||||
expect(await reopenHelpCenterTicket(ticketId("5"))).toEqual({
|
||||
ok: false,
|
||||
error: "Ticket not found",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports an already open ticket", async () => {
|
||||
mocks.selectRows = [{ id: 5, open: true }];
|
||||
|
||||
expect(await reopenHelpCenterTicket(ticketId("5"))).toEqual({
|
||||
ok: false,
|
||||
error: "Ticket is already open",
|
||||
});
|
||||
expect(mocks.updateSet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects callers without the ticket permission", async () => {
|
||||
mocks.canAccess.mockReturnValue(false);
|
||||
|
||||
expect(await reopenHelpCenterTicket(ticketId("5"))).toEqual({
|
||||
ok: false,
|
||||
error: "Unauthorized",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user