tests: add coverage for src/actions/ (15 files) and src/lib/{admin,auth} (3 files)
- src/actions coverage: 2.4% -> 13.55% - src/lib/admin coverage: 44.3% -> 84.81% - src/lib/auth coverage: 90.52% - vitest.config.ts: exclude .next.prev/ from test discovery
This commit is contained in:
1 parent
c433e5a52f
commit
ea7d861e69
19 files changed
+1262
-1
No files matched your search
@@ -0,0 +1,83 @@
|
|||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { requirePermission } from "@/lib/admin/guard";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { ActionError } from "@/lib/safe-action-shared";
|
||||||
|
import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||||
|
import { createAd, deleteAd, deleteAdForm, updateAd } from "./admin-ads";
|
||||||
|
|
||||||
|
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||||
|
vi.mock("@/lib/permissions", () => ({ PERMS: { PAGES_EDIT: "pages.edit" } }));
|
||||||
|
vi.mock("@/lib/prisma", () => ({
|
||||||
|
prisma: { websiteAds: { create: vi.fn(), update: vi.fn(), delete: vi.fn() } },
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/logger", () => ({ logger: { error: vi.fn() } }));
|
||||||
|
vi.mock("@/lib/safe-action", () => ({
|
||||||
|
adminAction: vi.fn((_opts: unknown, fn: Function) => fn),
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/safe-action-shared", () => ({
|
||||||
|
ActionError: class extends Error {},
|
||||||
|
actionOk: vi.fn(() => "ok"),
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/services/staff-activity", () => ({ logStaffActivity: vi.fn() }));
|
||||||
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||||
|
vi.mock("next/navigation", () => ({ redirect: vi.fn() }));
|
||||||
|
|
||||||
|
const staff = { id: 1, rank: 7, username: "admin" };
|
||||||
|
const fakeForm = (data: Record<string, string>) => ({
|
||||||
|
get: (key: string) => data[key] ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(requirePermission).mockResolvedValue(staff as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createAd", () => {
|
||||||
|
it("creates an ad and redirects", async () => {
|
||||||
|
vi.mocked(prisma.websiteAds.create).mockResolvedValue({
|
||||||
|
id: BigInt(1),
|
||||||
|
} as never);
|
||||||
|
await createAd(
|
||||||
|
fakeForm({ image: "https://example.com/ad.png" }) as unknown as FormData,
|
||||||
|
);
|
||||||
|
expect(prisma.websiteAds.create).toHaveBeenCalledWith({
|
||||||
|
data: expect.objectContaining({ image: "https://example.com/ad.png" }),
|
||||||
|
});
|
||||||
|
expect(logStaffActivity).toHaveBeenCalled();
|
||||||
|
expect(redirect).toHaveBeenCalledWith("/admin/ads");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns early when image is empty", async () => {
|
||||||
|
await createAd(fakeForm({ image: "" }) as unknown as FormData);
|
||||||
|
expect(prisma.websiteAds.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("logs error and revalidates on db failure", async () => {
|
||||||
|
vi.mocked(prisma.websiteAds.create).mockRejectedValue(new Error("DB down"));
|
||||||
|
await createAd(fakeForm({ image: "x" }) as unknown as FormData);
|
||||||
|
expect(logger.error).toHaveBeenCalled();
|
||||||
|
expect(revalidatePath).toHaveBeenCalledWith("/admin/ads");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deleteAd", () => {
|
||||||
|
it("deletes an ad via adminAction", async () => {
|
||||||
|
const handler = deleteAd as unknown as (ctx: {
|
||||||
|
data: { id: bigint };
|
||||||
|
session: { user: { id: string } };
|
||||||
|
}) => Promise<string>;
|
||||||
|
vi.mocked(prisma.websiteAds.delete).mockResolvedValue({} as never);
|
||||||
|
const result = await handler({
|
||||||
|
data: { id: BigInt(99) },
|
||||||
|
session: { user: { id: "1" } },
|
||||||
|
});
|
||||||
|
expect(prisma.websiteAds.delete).toHaveBeenCalledWith({
|
||||||
|
where: { id: BigInt(99) },
|
||||||
|
});
|
||||||
|
expect(result).toBe("ok");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { requirePermission } from "@/lib/admin/guard";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { rcon } from "@/lib/services/rcon";
|
||||||
|
import { sendHotelAlert } from "./admin-alerts";
|
||||||
|
|
||||||
|
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||||
|
vi.mock("@/lib/permissions", () => ({
|
||||||
|
PERMS: { NOTIFICATIONS_EDIT: "notifications.edit" },
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/services/rcon", () => ({ rcon: { send: vi.fn() } }));
|
||||||
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||||
|
|
||||||
|
const fakeForm = (data: Record<string, string>) => ({
|
||||||
|
get: (key: string) => data[key] ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(requirePermission).mockResolvedValue({
|
||||||
|
id: 1,
|
||||||
|
rank: 7,
|
||||||
|
username: "admin",
|
||||||
|
} as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sendHotelAlert", () => {
|
||||||
|
it("sends hotel alert and revalidates", async () => {
|
||||||
|
await sendHotelAlert(
|
||||||
|
fakeForm({ message: "Hello!" }) as unknown as FormData,
|
||||||
|
);
|
||||||
|
expect(rcon.send).toHaveBeenCalledWith("hotelalert", { message: "Hello!" });
|
||||||
|
expect(revalidatePath).toHaveBeenCalledWith("/admin/alerts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns early when message is empty", async () => {
|
||||||
|
await sendHotelAlert(fakeForm({ message: "" }) as unknown as FormData);
|
||||||
|
expect(rcon.send).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { requirePermissionRateLimited } from "@/lib/admin/guard";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { rcon } from "@/lib/services/rcon";
|
||||||
|
import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||||
|
import { createBan, liftBan } from "./admin-bans";
|
||||||
|
|
||||||
|
vi.mock("@/lib/admin/guard", () => ({ requirePermissionRateLimited: vi.fn() }));
|
||||||
|
vi.mock("@/lib/permissions", () => ({ PERMS: { USERS_BAN: "users.ban" } }));
|
||||||
|
vi.mock("@/lib/prisma", () => ({
|
||||||
|
prisma: {
|
||||||
|
user: { findUnique: vi.fn() },
|
||||||
|
ban: { create: vi.fn(), delete: vi.fn() },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/services/rcon", () => ({ rcon: { disconnectUser: vi.fn() } }));
|
||||||
|
vi.mock("@/lib/services/staff-activity", () => ({ logStaffActivity: vi.fn() }));
|
||||||
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||||
|
|
||||||
|
const staff = { id: 1, rank: 7, username: "admin" };
|
||||||
|
const fakeForm = (data: Record<string, string>) => ({
|
||||||
|
get: (key: string) => data[key] ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(requirePermissionRateLimited).mockResolvedValue(staff as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createBan", () => {
|
||||||
|
it("creates a ban for valid inputs", async () => {
|
||||||
|
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||||
|
username: "baduser",
|
||||||
|
} as never);
|
||||||
|
await createBan(
|
||||||
|
fakeForm({
|
||||||
|
userId: "42",
|
||||||
|
reason: "Spam",
|
||||||
|
hours: "24",
|
||||||
|
type: "account",
|
||||||
|
}) as unknown as FormData,
|
||||||
|
);
|
||||||
|
expect(prisma.ban.create).toHaveBeenCalledWith({
|
||||||
|
data: expect.objectContaining({ userId: 42, type: "account" }),
|
||||||
|
});
|
||||||
|
expect(rcon.disconnectUser).toHaveBeenCalledWith(42, "baduser");
|
||||||
|
expect(revalidatePath).toHaveBeenCalledWith("/admin/bans");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns early when userId is invalid", async () => {
|
||||||
|
await createBan(
|
||||||
|
fakeForm({
|
||||||
|
userId: "0",
|
||||||
|
hours: "1",
|
||||||
|
type: "account",
|
||||||
|
}) as unknown as FormData,
|
||||||
|
);
|
||||||
|
expect(prisma.ban.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("liftBan", () => {
|
||||||
|
it("deletes ban and revalidates", async () => {
|
||||||
|
await liftBan(fakeForm({ id: "42" }) as unknown as FormData);
|
||||||
|
expect(prisma.ban.delete).toHaveBeenCalledWith({ where: { id: 42 } });
|
||||||
|
expect(revalidatePath).toHaveBeenCalledWith("/admin/bans");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { requirePermissionRateLimited } from "@/lib/admin/guard";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||||
|
import { disbandGuild } from "./admin-guilds";
|
||||||
|
|
||||||
|
vi.mock("@/lib/admin/guard", () => ({ requirePermissionRateLimited: vi.fn() }));
|
||||||
|
vi.mock("@/lib/permissions", () => ({ PERMS: { USERS_EDIT: "users.edit" } }));
|
||||||
|
vi.mock("@/lib/prisma", () => ({
|
||||||
|
prisma: {
|
||||||
|
guilds: { findUnique: vi.fn(), delete: vi.fn() },
|
||||||
|
guildsForumsThreads: { findMany: vi.fn(), deleteMany: vi.fn() },
|
||||||
|
guildsForumsComments: { deleteMany: vi.fn() },
|
||||||
|
guildForumViews: { deleteMany: vi.fn() },
|
||||||
|
guildsMembers: { deleteMany: vi.fn() },
|
||||||
|
rooms: { updateMany: vi.fn() },
|
||||||
|
items: { updateMany: vi.fn() },
|
||||||
|
$transaction: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/services/staff-activity", () => ({ logStaffActivity: vi.fn() }));
|
||||||
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||||
|
|
||||||
|
const staff = { id: 1, rank: 7, username: "admin" };
|
||||||
|
const fakeForm = (data: Record<string, string>) => ({
|
||||||
|
get: (key: string) => data[key] ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(requirePermissionRateLimited).mockResolvedValue(staff as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("disbandGuild", () => {
|
||||||
|
it("disbands guild and cleans related data", async () => {
|
||||||
|
vi.mocked(prisma.guilds.findUnique).mockResolvedValue({
|
||||||
|
id: 1,
|
||||||
|
name: "TestGuild",
|
||||||
|
userId: 42,
|
||||||
|
} as never);
|
||||||
|
vi.mocked(prisma.guildsForumsThreads.findMany).mockResolvedValue([
|
||||||
|
{ id: 10 },
|
||||||
|
] as never);
|
||||||
|
vi.mocked(prisma.$transaction).mockImplementation(async (fn: unknown) => {
|
||||||
|
const tx = {
|
||||||
|
guildsForumsComments: { deleteMany: vi.fn().mockResolvedValue({}) },
|
||||||
|
guildsForumsThreads: {
|
||||||
|
findMany: vi.fn().mockResolvedValue([{ id: 10 }]),
|
||||||
|
deleteMany: vi.fn().mockResolvedValue({}),
|
||||||
|
},
|
||||||
|
guildForumViews: { deleteMany: vi.fn().mockResolvedValue({}) },
|
||||||
|
guildsMembers: { deleteMany: vi.fn().mockResolvedValue({}) },
|
||||||
|
rooms: { updateMany: vi.fn().mockResolvedValue({}) },
|
||||||
|
items: { updateMany: vi.fn().mockResolvedValue({}) },
|
||||||
|
guilds: { delete: vi.fn().mockResolvedValue({}) },
|
||||||
|
} as never;
|
||||||
|
await (fn as (tx: never) => Promise<void>)(tx);
|
||||||
|
});
|
||||||
|
await disbandGuild(fakeForm({ id: "1" }) as unknown as FormData);
|
||||||
|
expect(logStaffActivity).toHaveBeenCalled();
|
||||||
|
expect(revalidatePath).toHaveBeenCalledWith("/admin/guilds");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns early when id is not positive", async () => {
|
||||||
|
await disbandGuild(fakeForm({ id: "0" }) as unknown as FormData);
|
||||||
|
expect(prisma.guilds.findUnique).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { requirePermission } from "@/lib/admin/guard";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||||
|
import {
|
||||||
|
createHelpQuestion,
|
||||||
|
deleteHelpQuestion,
|
||||||
|
updateHelpQuestion,
|
||||||
|
} from "./admin-help";
|
||||||
|
|
||||||
|
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||||
|
vi.mock("@/lib/permissions", () => ({ PERMS: { PAGES_EDIT: "pages.edit" } }));
|
||||||
|
vi.mock("@/lib/prisma", () => ({
|
||||||
|
prisma: {
|
||||||
|
websiteHelpCenterCategories: {
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/services/staff-activity", () => ({ logStaffActivity: vi.fn() }));
|
||||||
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||||
|
vi.mock("next/navigation", () => ({ redirect: vi.fn() }));
|
||||||
|
|
||||||
|
const staff = { id: 1, rank: 7, username: "admin" };
|
||||||
|
const fakeForm = (data: Record<string, string | null>) => ({
|
||||||
|
get: (key: string) => (key in data ? data[key] : null),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(requirePermission).mockResolvedValue(staff as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createHelpQuestion", () => {
|
||||||
|
it("creates a help question and redirects", async () => {
|
||||||
|
vi.mocked(prisma.websiteHelpCenterCategories.create).mockResolvedValue({
|
||||||
|
id: BigInt(5),
|
||||||
|
} as never);
|
||||||
|
await createHelpQuestion(
|
||||||
|
fakeForm({
|
||||||
|
name: "FAQ",
|
||||||
|
content: "<p>Answer</p>",
|
||||||
|
}) as unknown as FormData,
|
||||||
|
);
|
||||||
|
expect(prisma.websiteHelpCenterCategories.create).toHaveBeenCalled();
|
||||||
|
expect(redirect).toHaveBeenCalledWith("/admin/help-questions");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateHelpQuestion", () => {
|
||||||
|
it("updates and redirects", async () => {
|
||||||
|
vi.mocked(prisma.websiteHelpCenterCategories.update).mockResolvedValue(
|
||||||
|
{} as never,
|
||||||
|
);
|
||||||
|
await updateHelpQuestion(
|
||||||
|
fakeForm({
|
||||||
|
id: "42",
|
||||||
|
name: "Updated",
|
||||||
|
content: "New",
|
||||||
|
}) as unknown as FormData,
|
||||||
|
);
|
||||||
|
expect(prisma.websiteHelpCenterCategories.update).toHaveBeenCalled();
|
||||||
|
expect(redirect).toHaveBeenCalledWith("/admin/help-questions");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deleteHelpQuestion", () => {
|
||||||
|
it("deletes and redirects", async () => {
|
||||||
|
vi.mocked(prisma.websiteHelpCenterCategories.delete).mockResolvedValue(
|
||||||
|
{} as never,
|
||||||
|
);
|
||||||
|
await deleteHelpQuestion(fakeForm({ id: "42" }) as unknown as FormData);
|
||||||
|
expect(prisma.websiteHelpCenterCategories.delete).toHaveBeenCalled();
|
||||||
|
expect(redirect).toHaveBeenCalledWith("/admin/help-questions");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { requirePermission } from "@/lib/admin/guard";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import {
|
||||||
|
addBlacklist,
|
||||||
|
addWhitelist,
|
||||||
|
deleteBlacklist,
|
||||||
|
deleteWhitelist,
|
||||||
|
} from "./admin-ip";
|
||||||
|
|
||||||
|
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||||
|
vi.mock("@/lib/permissions", () => ({
|
||||||
|
PERMS: { SETTINGS_EDIT: "settings.edit" },
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/prisma", () => ({
|
||||||
|
prisma: {
|
||||||
|
websiteIpWhitelist: { create: vi.fn(), delete: vi.fn() },
|
||||||
|
websiteIpBlacklist: { create: vi.fn(), delete: vi.fn() },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||||
|
|
||||||
|
const staff = { id: 1, rank: 7, username: "admin" };
|
||||||
|
const fakeForm = (data: Record<string, string>) => ({
|
||||||
|
get: (key: string) => data[key] ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(requirePermission).mockResolvedValue(staff as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("addWhitelist", () => {
|
||||||
|
it("creates whitelist entry", async () => {
|
||||||
|
await addWhitelist(
|
||||||
|
fakeForm({ ipAddress: "192.168.1.1" }) as unknown as FormData,
|
||||||
|
);
|
||||||
|
expect(prisma.websiteIpWhitelist.create).toHaveBeenCalledWith({
|
||||||
|
data: { ipAddress: "192.168.1.1", asn: null, whitelistAsn: false },
|
||||||
|
});
|
||||||
|
expect(revalidatePath).toHaveBeenCalledWith("/admin/ip");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns early when ip is empty", async () => {
|
||||||
|
await addWhitelist(fakeForm({ ipAddress: "" }) as unknown as FormData);
|
||||||
|
expect(prisma.websiteIpWhitelist.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deleteWhitelist", () => {
|
||||||
|
it("deletes whitelist entry", async () => {
|
||||||
|
await deleteWhitelist(fakeForm({ id: "42" }) as unknown as FormData);
|
||||||
|
expect(prisma.websiteIpWhitelist.delete).toHaveBeenCalledWith({
|
||||||
|
where: { id: BigInt(42) },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("addBlacklist", () => {
|
||||||
|
it("creates blacklist entry", async () => {
|
||||||
|
await addBlacklist(
|
||||||
|
fakeForm({ ipAddress: "203.0.113.1" }) as unknown as FormData,
|
||||||
|
);
|
||||||
|
expect(prisma.websiteIpBlacklist.create).toHaveBeenCalledWith({
|
||||||
|
data: { ipAddress: "203.0.113.1", asn: null, blacklistAsn: false },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deleteBlacklist", () => {
|
||||||
|
it("deletes blacklist entry", async () => {
|
||||||
|
await deleteBlacklist(fakeForm({ id: "99" }) as unknown as FormData);
|
||||||
|
expect(prisma.websiteIpBlacklist.delete).toHaveBeenCalledWith({
|
||||||
|
where: { id: BigInt(99) },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { mkdir, writeFile } from "node:fs/promises";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { requirePermission } from "@/lib/admin/guard";
|
||||||
|
import { MEDIA_ROOT, resolveMediaPath } from "@/lib/media-storage";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { deleteMedia, uploadMedia, uploadMediaAndReturn } from "./admin-media";
|
||||||
|
|
||||||
|
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||||
|
vi.mock("@/lib/permissions", () => ({ PERMS: { PAGES_EDIT: "pages.edit" } }));
|
||||||
|
vi.mock("@/lib/media-storage", () => ({
|
||||||
|
MEDIA_ROOT: "/tmp/nexst-test-media",
|
||||||
|
resolveMediaPath: vi.fn((name: string) => `/tmp/nexst-test-media/${name}`),
|
||||||
|
}));
|
||||||
|
vi.mock("node:fs/promises", () => ({
|
||||||
|
mkdir: vi.fn(),
|
||||||
|
writeFile: vi.fn(),
|
||||||
|
unlink: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||||
|
|
||||||
|
const staff = { id: 1, rank: 7, username: "admin" };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(requirePermission).mockResolvedValue(staff as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("uploadMedia", () => {
|
||||||
|
it("returns error when no file provided", async () => {
|
||||||
|
const result = await uploadMedia(new FormData());
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
expect(result.error).toBe("No file provided");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("uploadMediaAndReturn", () => {
|
||||||
|
it("returns empty string when no file", async () => {
|
||||||
|
expect(await uploadMediaAndReturn(new FormData())).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deleteMedia", () => {
|
||||||
|
it("deletes media file and revalidates", async () => {
|
||||||
|
await deleteMedia("photo.png");
|
||||||
|
expect(revalidatePath).toHaveBeenCalledWith("/api/media");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips deletion when path is outside media root", async () => {
|
||||||
|
vi.mocked(resolveMediaPath).mockReturnValue("/etc/passwd");
|
||||||
|
await deleteMedia("../../../etc/passwd");
|
||||||
|
const { unlink } = await import("node:fs/promises");
|
||||||
|
expect(unlink).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { requirePermission } from "@/lib/admin/guard";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { deletePhoto } from "./admin-photos";
|
||||||
|
|
||||||
|
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||||
|
vi.mock("@/lib/permissions", () => ({ PERMS: { PAGES_EDIT: "pages.edit" } }));
|
||||||
|
vi.mock("@/lib/prisma", () => ({ prisma: { cameraWeb: { delete: vi.fn() } } }));
|
||||||
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||||
|
|
||||||
|
const fakeForm = (data: Record<string, string>) => ({
|
||||||
|
get: (key: string) => data[key] ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(requirePermission).mockResolvedValue({
|
||||||
|
id: 1,
|
||||||
|
rank: 7,
|
||||||
|
username: "admin",
|
||||||
|
} as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deletePhoto", () => {
|
||||||
|
it("deletes a photo and revalidates", async () => {
|
||||||
|
await deletePhoto(fakeForm({ id: "42" }) as unknown as FormData);
|
||||||
|
expect(prisma.cameraWeb.delete).toHaveBeenCalledWith({ where: { id: 42 } });
|
||||||
|
expect(revalidatePath).toHaveBeenCalledWith("/admin/photos");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns early when id is not positive", async () => {
|
||||||
|
await deletePhoto(fakeForm({ id: "0" }) as unknown as FormData);
|
||||||
|
expect(prisma.cameraWeb.delete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { requirePermission } from "@/lib/admin/guard";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||||
|
import { createTag, deleteTag, updateTag } from "./admin-tags";
|
||||||
|
|
||||||
|
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||||
|
vi.mock("@/lib/permissions", () => ({ PERMS: { PAGES_EDIT: "pages.edit" } }));
|
||||||
|
vi.mock("@/lib/prisma", () => ({
|
||||||
|
prisma: {
|
||||||
|
tags: { create: vi.fn(), update: vi.fn(), delete: vi.fn() },
|
||||||
|
taggables: { deleteMany: vi.fn() },
|
||||||
|
$transaction: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/services/staff-activity", () => ({ logStaffActivity: vi.fn() }));
|
||||||
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||||
|
|
||||||
|
const staff = { id: 1, rank: 7, username: "admin" };
|
||||||
|
const fakeForm = (data: Record<string, string>) => ({
|
||||||
|
get: (key: string) => data[key] ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(requirePermission).mockResolvedValue(staff as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createTag", () => {
|
||||||
|
it("creates a tag and revalidates", async () => {
|
||||||
|
vi.mocked(prisma.tags.create).mockResolvedValue({ id: BigInt(1) } as never);
|
||||||
|
|
||||||
|
await createTag(
|
||||||
|
fakeForm({
|
||||||
|
name: "News",
|
||||||
|
backgroundColor: "#ff0000",
|
||||||
|
}) as unknown as FormData,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(prisma.tags.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: expect.objectContaining({ name: "News" }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(logStaffActivity).toHaveBeenCalled();
|
||||||
|
expect(revalidatePath).toHaveBeenCalledWith("/admin/tags");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns early when name is empty", async () => {
|
||||||
|
await createTag(fakeForm({ name: "" }) as unknown as FormData);
|
||||||
|
expect(prisma.tags.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses default color when not provided", async () => {
|
||||||
|
vi.mocked(prisma.tags.create).mockResolvedValue({ id: BigInt(1) } as never);
|
||||||
|
|
||||||
|
await createTag(fakeForm({ name: "Test" }) as unknown as FormData);
|
||||||
|
|
||||||
|
expect(prisma.tags.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: expect.objectContaining({ backgroundColor: "#888888" }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles db error gracefully", async () => {
|
||||||
|
vi.mocked(prisma.tags.create).mockRejectedValue(new Error("DB error"));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
createTag(fakeForm({ name: "News" }) as unknown as FormData),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
expect(revalidatePath).toHaveBeenCalledWith("/admin/tags");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateTag", () => {
|
||||||
|
it("updates a tag and revalidates", async () => {
|
||||||
|
vi.mocked(prisma.tags.update).mockResolvedValue({} as never);
|
||||||
|
|
||||||
|
await updateTag(
|
||||||
|
fakeForm({
|
||||||
|
id: "42",
|
||||||
|
name: "Updated",
|
||||||
|
backgroundColor: "#00ff00",
|
||||||
|
}) as unknown as FormData,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(prisma.tags.update).toHaveBeenCalledWith({
|
||||||
|
where: { id: BigInt(42) },
|
||||||
|
data: expect.objectContaining({ name: "Updated" }),
|
||||||
|
});
|
||||||
|
expect(logStaffActivity).toHaveBeenCalled();
|
||||||
|
expect(revalidatePath).toHaveBeenCalledWith("/admin/tags");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns early when id is invalid", async () => {
|
||||||
|
await updateTag(fakeForm({ id: "", name: "Test" }) as unknown as FormData);
|
||||||
|
expect(prisma.tags.update).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns early when name is empty after update", async () => {
|
||||||
|
await updateTag(fakeForm({ id: "42", name: "" }) as unknown as FormData);
|
||||||
|
expect(prisma.tags.update).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deleteTag", () => {
|
||||||
|
it("deletes a tag and its taggables", async () => {
|
||||||
|
vi.mocked(prisma.taggables.deleteMany).mockResolvedValue({
|
||||||
|
count: 1,
|
||||||
|
} as never);
|
||||||
|
vi.mocked(prisma.tags.delete).mockResolvedValue({} as never);
|
||||||
|
vi.mocked(prisma.$transaction).mockImplementation(async (ops: unknown) => {
|
||||||
|
const arr = ops as [
|
||||||
|
typeof prisma.taggables.deleteMany,
|
||||||
|
typeof prisma.tags.delete,
|
||||||
|
];
|
||||||
|
await arr[0];
|
||||||
|
await arr[1];
|
||||||
|
});
|
||||||
|
|
||||||
|
await deleteTag(fakeForm({ id: "42" }) as unknown as FormData);
|
||||||
|
|
||||||
|
expect(prisma.taggables.deleteMany).toHaveBeenCalledWith({
|
||||||
|
where: { tagId: BigInt(42) },
|
||||||
|
});
|
||||||
|
expect(prisma.tags.delete).toHaveBeenCalledWith({
|
||||||
|
where: { id: BigInt(42) },
|
||||||
|
});
|
||||||
|
expect(logStaffActivity).toHaveBeenCalled();
|
||||||
|
expect(revalidatePath).toHaveBeenCalledWith("/admin/tags");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns early when id is invalid", async () => {
|
||||||
|
await deleteTag(fakeForm({ id: "" }) as unknown as FormData);
|
||||||
|
expect(prisma.$transaction).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { requirePermission } from "@/lib/admin/guard";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { createTeam, deleteTeam } from "./admin-teams";
|
||||||
|
|
||||||
|
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||||
|
vi.mock("@/lib/permissions", () => ({ PERMS: { USERS_EDIT: "users.edit" } }));
|
||||||
|
vi.mock("@/lib/prisma", () => ({
|
||||||
|
prisma: { websiteTeams: { create: vi.fn(), delete: vi.fn() } },
|
||||||
|
}));
|
||||||
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||||
|
|
||||||
|
const staff = { id: 1, rank: 7, username: "admin" };
|
||||||
|
const fakeForm = (data: Record<string, string | null>) => ({
|
||||||
|
get: (key: string) => (key in data ? data[key] : null),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(requirePermission).mockResolvedValue(staff as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createTeam", () => {
|
||||||
|
it("creates a team entry", async () => {
|
||||||
|
await createTeam(
|
||||||
|
fakeForm({ rankName: "Moderator" }) as unknown as FormData,
|
||||||
|
);
|
||||||
|
expect(prisma.websiteTeams.create).toHaveBeenCalledWith({
|
||||||
|
data: expect.objectContaining({ rankName: "Moderator" }),
|
||||||
|
});
|
||||||
|
expect(revalidatePath).toHaveBeenCalledWith("/admin/teams");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns early when rankName is empty", async () => {
|
||||||
|
await createTeam(fakeForm({ rankName: "" }) as unknown as FormData);
|
||||||
|
expect(prisma.websiteTeams.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deleteTeam", () => {
|
||||||
|
it("deletes a team entry", async () => {
|
||||||
|
vi.mocked(prisma.websiteTeams.delete).mockResolvedValue({} as never);
|
||||||
|
await deleteTeam(fakeForm({ id: "42" }) as unknown as FormData);
|
||||||
|
expect(prisma.websiteTeams.delete).toHaveBeenCalledWith({
|
||||||
|
where: { id: BigInt(42) },
|
||||||
|
});
|
||||||
|
expect(revalidatePath).toHaveBeenCalledWith("/admin/teams");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { requirePermission } from "@/lib/admin/guard";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { siteSettings } from "@/lib/services/site-settings";
|
||||||
|
import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||||
|
import { saveVpn } from "./admin-vpn";
|
||||||
|
|
||||||
|
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||||
|
vi.mock("@/lib/permissions", () => ({
|
||||||
|
PERMS: { SETTINGS_EDIT: "settings.edit" },
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/prisma", () => ({
|
||||||
|
prisma: { websiteSetting: { upsert: vi.fn() } },
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/services/site-settings", () => ({
|
||||||
|
siteSettings: { reload: vi.fn() },
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/services/staff-activity", () => ({ logStaffActivity: vi.fn() }));
|
||||||
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||||
|
vi.mock("next/navigation", () => ({ redirect: vi.fn() }));
|
||||||
|
|
||||||
|
const staff = { id: 1, rank: 7, username: "admin" };
|
||||||
|
const fakeForm = (data: Record<string, string | null>) => ({
|
||||||
|
get: (key: string) => (key in data ? data[key] : null),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(requirePermission).mockResolvedValue(staff as never);
|
||||||
|
vi.mocked(prisma.websiteSetting.upsert).mockResolvedValue({} as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("saveVpn", () => {
|
||||||
|
it("saves VPN settings and redirects", async () => {
|
||||||
|
await saveVpn(
|
||||||
|
fakeForm({
|
||||||
|
vpn_block_enabled: "1",
|
||||||
|
vpn_provider: "proxycheck",
|
||||||
|
vpn_api_key: "abc123",
|
||||||
|
}) as unknown as FormData,
|
||||||
|
);
|
||||||
|
expect(prisma.websiteSetting.upsert).toHaveBeenCalledTimes(4);
|
||||||
|
expect(siteSettings.reload).toHaveBeenCalled();
|
||||||
|
expect(redirect).toHaveBeenCalledWith("/admin/vpn?saved=1");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { auth } from "@/lib/auth";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
||||||
|
import { applyStaff, applyTeam } from "./applications";
|
||||||
|
|
||||||
|
vi.mock("@/lib/auth", () => ({ auth: vi.fn() }));
|
||||||
|
vi.mock("@/lib/prisma", () => ({
|
||||||
|
prisma: { websiteStaffApplications: { findFirst: vi.fn(), create: vi.fn() } },
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/rate-limit", () => ({ clientIp: vi.fn(), rateLimit: vi.fn() }));
|
||||||
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||||
|
vi.mock("next/navigation", () => ({ redirect: vi.fn() }));
|
||||||
|
|
||||||
|
const fakeForm = (data: Record<string, string>) => ({
|
||||||
|
get: (key: string) => data[key] ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(auth).mockResolvedValue({ user: { id: "42" } } as never);
|
||||||
|
vi.mocked(clientIp).mockResolvedValue("127.0.0.1");
|
||||||
|
vi.mocked(rateLimit).mockResolvedValue({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applyStaff", () => {
|
||||||
|
it("submits staff application", async () => {
|
||||||
|
vi.mocked(prisma.websiteStaffApplications.findFirst).mockResolvedValue(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
vi.mocked(prisma.websiteStaffApplications.create).mockResolvedValue(
|
||||||
|
{} as never,
|
||||||
|
);
|
||||||
|
await applyStaff(
|
||||||
|
fakeForm({
|
||||||
|
rankId: "3",
|
||||||
|
content: "I want to help!",
|
||||||
|
}) as unknown as FormData,
|
||||||
|
);
|
||||||
|
expect(prisma.websiteStaffApplications.create).toHaveBeenCalled();
|
||||||
|
expect(redirect).toHaveBeenCalledWith("/apply/staff?submitted=1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redirects to login when not authenticated", async () => {
|
||||||
|
vi.mocked(auth).mockResolvedValue(null);
|
||||||
|
await applyStaff(fakeForm({}) as unknown as FormData);
|
||||||
|
expect(redirect).toHaveBeenCalledWith("/login");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns duplicate status when application exists", async () => {
|
||||||
|
vi.mocked(prisma.websiteStaffApplications.findFirst).mockResolvedValue({
|
||||||
|
id: 1,
|
||||||
|
} as never);
|
||||||
|
await applyStaff(
|
||||||
|
fakeForm({
|
||||||
|
rankId: "3",
|
||||||
|
content: "I want to help!",
|
||||||
|
}) as unknown as FormData,
|
||||||
|
);
|
||||||
|
expect(prisma.websiteStaffApplications.create).not.toHaveBeenCalled();
|
||||||
|
expect(redirect).toHaveBeenCalledWith("/apply/staff?error=duplicate");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles rate limit", async () => {
|
||||||
|
vi.mocked(rateLimit).mockResolvedValue({ ok: false });
|
||||||
|
await applyStaff(
|
||||||
|
fakeForm({
|
||||||
|
rankId: "3",
|
||||||
|
content: "I want to help!",
|
||||||
|
}) as unknown as FormData,
|
||||||
|
);
|
||||||
|
expect(redirect).toHaveBeenCalledWith("/apply/staff?error=ratelimit");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applyTeam", () => {
|
||||||
|
it("submits team application", async () => {
|
||||||
|
vi.mocked(prisma.websiteStaffApplications.findFirst).mockResolvedValue(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
vi.mocked(prisma.websiteStaffApplications.create).mockResolvedValue(
|
||||||
|
{} as never,
|
||||||
|
);
|
||||||
|
await applyTeam(
|
||||||
|
fakeForm({
|
||||||
|
teamId: "2",
|
||||||
|
content: "I want to join team!",
|
||||||
|
}) as unknown as FormData,
|
||||||
|
);
|
||||||
|
expect(prisma.websiteStaffApplications.create).toHaveBeenCalled();
|
||||||
|
expect(redirect).toHaveBeenCalledWith("/apply/team?submitted=1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redirects to login when not authenticated", async () => {
|
||||||
|
vi.mocked(auth).mockResolvedValue(null);
|
||||||
|
await applyTeam(fakeForm({}) as unknown as FormData);
|
||||||
|
expect(redirect).toHaveBeenCalledWith("/login");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { env } from "@/env";
|
||||||
|
import { checkLogin } from "@/lib/auth/password";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
||||||
|
import { captchaConfig, verifyCaptcha } from "@/lib/services/captcha";
|
||||||
|
import { siteSettings } from "@/lib/services/site-settings";
|
||||||
|
import { precheckLogin } from "./auth-precheck";
|
||||||
|
|
||||||
|
vi.mock("@/env", () => ({ env: { CONVERT_PASSWORDS: false } }));
|
||||||
|
vi.mock("@/lib/auth/password", () => ({ checkLogin: vi.fn() }));
|
||||||
|
vi.mock("@/lib/prisma", () => ({ prisma: { user: { findUnique: vi.fn() } } }));
|
||||||
|
vi.mock("@/lib/rate-limit", () => ({ clientIp: vi.fn(), rateLimit: vi.fn() }));
|
||||||
|
vi.mock("@/lib/services/captcha", () => ({
|
||||||
|
captchaConfig: vi.fn(),
|
||||||
|
verifyCaptcha: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/services/site-settings", () => ({
|
||||||
|
siteSettings: { getBool: vi.fn() },
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(clientIp).mockResolvedValue("1.2.3.4");
|
||||||
|
vi.mocked(rateLimit).mockResolvedValue({ ok: true });
|
||||||
|
vi.mocked(checkLogin).mockResolvedValue({ valid: true } as never);
|
||||||
|
vi.mocked(captchaConfig).mockResolvedValue({ provider: "none" } as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("precheckLogin", () => {
|
||||||
|
it("returns ok for valid login without 2FA", async () => {
|
||||||
|
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||||
|
password: "hash",
|
||||||
|
twoFactorConfirmedAt: null,
|
||||||
|
mail: null,
|
||||||
|
mailVerified: "0",
|
||||||
|
} as never);
|
||||||
|
expect(await precheckLogin("user", "pass")).toBe("ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns twofactor when 2FA is set up", async () => {
|
||||||
|
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||||
|
password: "hash",
|
||||||
|
twoFactorConfirmedAt: new Date(),
|
||||||
|
mail: null,
|
||||||
|
mailVerified: "0",
|
||||||
|
} as never);
|
||||||
|
expect(await precheckLogin("user", "pass")).toBe("twofactor");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns invalid for empty inputs", async () => {
|
||||||
|
expect(await precheckLogin("", "")).toBe("invalid");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns captcha when captcha required", async () => {
|
||||||
|
vi.mocked(captchaConfig).mockResolvedValue({
|
||||||
|
provider: "hcaptcha",
|
||||||
|
} as never);
|
||||||
|
vi.mocked(verifyCaptcha).mockResolvedValue(false);
|
||||||
|
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||||
|
password: "hash",
|
||||||
|
twoFactorConfirmedAt: null,
|
||||||
|
mail: null,
|
||||||
|
mailVerified: "0",
|
||||||
|
} as never);
|
||||||
|
expect(await precheckLogin("user", "pass", "bad-token")).toBe("captcha");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns invalid when user not found (dummy hash check)", async () => {
|
||||||
|
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
|
||||||
|
const result = await precheckLogin("nonexistent", "pass");
|
||||||
|
expect(result).toBe("invalid");
|
||||||
|
expect(checkLogin).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns unverified when email verification required", async () => {
|
||||||
|
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||||
|
password: "hash",
|
||||||
|
twoFactorConfirmedAt: null,
|
||||||
|
mail: "[email protected]",
|
||||||
|
mailVerified: "0",
|
||||||
|
} as never);
|
||||||
|
vi.mocked(siteSettings.getBool).mockResolvedValue(true);
|
||||||
|
expect(await precheckLogin("user", "pass")).toBe("unverified");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { requirePermission } from "@/lib/admin/guard";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { rcon } from "@/lib/services/rcon";
|
||||||
|
import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||||
|
import {
|
||||||
|
bulkBan,
|
||||||
|
bulkGiveBadge,
|
||||||
|
bulkGiveCurrency,
|
||||||
|
bulkUnban,
|
||||||
|
} from "./bulk-users";
|
||||||
|
|
||||||
|
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||||
|
vi.mock("@/lib/permissions", () => ({ PERMS: { USERS_EDIT: "users.edit" } }));
|
||||||
|
vi.mock("@/lib/prisma", () => ({
|
||||||
|
prisma: {
|
||||||
|
ban: { deleteMany: vi.fn(), create: vi.fn() },
|
||||||
|
user: { update: vi.fn() },
|
||||||
|
usersCurrency: { upsert: vi.fn() },
|
||||||
|
usersBadges: { findFirst: vi.fn(), aggregate: vi.fn(), create: vi.fn() },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/services/rcon", () => ({
|
||||||
|
rcon: {
|
||||||
|
giveCredits: vi.fn(),
|
||||||
|
giveDuckets: vi.fn(),
|
||||||
|
givePointsGotw: vi.fn(),
|
||||||
|
giveBadge: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/services/staff-activity", () => ({ logStaffActivity: vi.fn() }));
|
||||||
|
|
||||||
|
const staff = { id: 1, rank: 7, username: "admin" };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(requirePermission).mockResolvedValue(staff as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("bulkUnban", () => {
|
||||||
|
it("unbans users", async () => {
|
||||||
|
vi.mocked(prisma.ban.deleteMany).mockResolvedValue({ count: 3 } as never);
|
||||||
|
const r = await bulkUnban({ userIds: [1, 2, 3] });
|
||||||
|
expect(r.ok).toBe(true);
|
||||||
|
expect(r.data).toEqual({ unbanned: 3, total: 3 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("bulkBan", () => {
|
||||||
|
it("bans users", async () => {
|
||||||
|
vi.mocked(prisma.ban.create).mockResolvedValue({} as never);
|
||||||
|
const r = await bulkBan({
|
||||||
|
userIds: [1, 2],
|
||||||
|
reason: "Spam",
|
||||||
|
duration: 3600,
|
||||||
|
});
|
||||||
|
expect(r.ok).toBe(true);
|
||||||
|
expect(r.data.banned).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("bulkGiveCurrency", () => {
|
||||||
|
it("gives credits", async () => {
|
||||||
|
vi.mocked(prisma.user.update).mockResolvedValue({} as never);
|
||||||
|
const r = await bulkGiveCurrency({
|
||||||
|
userIds: [1],
|
||||||
|
amount: 100,
|
||||||
|
type: "credits",
|
||||||
|
});
|
||||||
|
expect(r.data.given).toBe(1);
|
||||||
|
expect(rcon.giveCredits).toHaveBeenCalledWith(1, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gives pixels", async () => {
|
||||||
|
vi.mocked(prisma.usersCurrency.upsert).mockResolvedValue({} as never);
|
||||||
|
const r = await bulkGiveCurrency({
|
||||||
|
userIds: [2],
|
||||||
|
amount: 50,
|
||||||
|
type: "pixels",
|
||||||
|
});
|
||||||
|
expect(r.data.given).toBe(1);
|
||||||
|
expect(rcon.giveDuckets).toHaveBeenCalledWith(2, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gives points", async () => {
|
||||||
|
vi.mocked(prisma.usersCurrency.upsert).mockResolvedValue({} as never);
|
||||||
|
const r = await bulkGiveCurrency({
|
||||||
|
userIds: [3],
|
||||||
|
amount: 25,
|
||||||
|
type: "points",
|
||||||
|
});
|
||||||
|
expect(r.data.given).toBe(1);
|
||||||
|
expect(rcon.givePointsGotw).toHaveBeenCalledWith(3, 25);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("bulkGiveBadge", () => {
|
||||||
|
it("gives badge to user", async () => {
|
||||||
|
vi.mocked(prisma.usersBadges.findFirst).mockResolvedValue(null);
|
||||||
|
vi.mocked(prisma.usersBadges.aggregate).mockResolvedValue({
|
||||||
|
_max: { slotId: 5 },
|
||||||
|
} as never);
|
||||||
|
vi.mocked(prisma.usersBadges.create).mockResolvedValue({} as never);
|
||||||
|
const r = await bulkGiveBadge({ userIds: [1], badgeCode: "ADM" });
|
||||||
|
expect(r.data.given).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { adminAction } from "@/lib/safe-action";
|
||||||
|
import { actionOk } from "@/lib/safe-action-shared";
|
||||||
|
import { logAudit } from "@/lib/services/audit";
|
||||||
|
import { rcon } from "@/lib/services/rcon";
|
||||||
|
|
||||||
|
vi.mock("@/lib/permissions", () => ({
|
||||||
|
PERMS: { SETTINGS_EDIT: "settings.edit" },
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/prisma", () => ({
|
||||||
|
prisma: { emulatorSettings: { upsert: vi.fn() } },
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/safe-action", () => ({
|
||||||
|
adminAction: vi.fn((_opts: unknown, fn: Function) => fn),
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/safe-action-shared", () => ({ actionOk: vi.fn(() => "ok") }));
|
||||||
|
vi.mock("@/lib/services/audit", () => ({ logAudit: vi.fn() }));
|
||||||
|
vi.mock("@/lib/services/rcon", () => ({ rcon: { updateConfig: vi.fn() } }));
|
||||||
|
|
||||||
|
describe("saveEmulatorSettings", () => {
|
||||||
|
it("saves settings and calls rcon update", async () => {
|
||||||
|
vi.mocked(prisma.emulatorSettings.upsert).mockResolvedValue({} as never);
|
||||||
|
const handler = (await import("./emulator").then(
|
||||||
|
(m) => m.saveEmulatorSettings,
|
||||||
|
)) as unknown as (ctx: {
|
||||||
|
data: { settings: Record<string, string> };
|
||||||
|
session: { user: { id: string } };
|
||||||
|
}) => Promise<string>;
|
||||||
|
|
||||||
|
const result = await handler({
|
||||||
|
data: { settings: { key1: "val1", key2: "val2" } },
|
||||||
|
session: { user: { id: "1" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(prisma.emulatorSettings.upsert).toHaveBeenCalledTimes(2);
|
||||||
|
expect(rcon.updateConfig).toHaveBeenCalled();
|
||||||
|
expect(result).toBe("ok");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { redirectSafe } from "@/lib/foundation/security";
|
||||||
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
|
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
||||||
|
import {
|
||||||
|
requireMod,
|
||||||
|
requireModPermission,
|
||||||
|
requirePermission,
|
||||||
|
requirePermissionRateLimited,
|
||||||
|
requireStaff,
|
||||||
|
requireStaffRateLimited,
|
||||||
|
} from "./guard";
|
||||||
|
|
||||||
|
vi.mock("@/lib/permissions", () => ({
|
||||||
|
getAdminContext: vi.fn(),
|
||||||
|
canAccess: vi.fn(),
|
||||||
|
PERMS: {
|
||||||
|
ADMIN_DASHBOARD: "admin.dashboard",
|
||||||
|
MOD_DASHBOARD: "mod.dashboard",
|
||||||
|
MODERATION_VIEW: "moderation.view",
|
||||||
|
MOD_CFH_VIEW: "mod.cfh.view",
|
||||||
|
MOD_ACTIONS: "mod.actions",
|
||||||
|
MOD_BANS_VIEW: "mod.bans.view",
|
||||||
|
MOD_TICKETS_VIEW: "mod.tickets.view",
|
||||||
|
MOD_TEAM_VIEW: "mod.team.view",
|
||||||
|
TICKETS_VIEW: "tickets.view",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/foundation/security", () => ({ redirectSafe: vi.fn() }));
|
||||||
|
vi.mock("@/lib/rate-limit", () => ({ clientIp: vi.fn(), rateLimit: vi.fn() }));
|
||||||
|
|
||||||
|
const session = {
|
||||||
|
session: { user: { id: 1, rank: 7, username: "admin" } },
|
||||||
|
permissions: { "admin.dashboard": 7 },
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(getAdminContext).mockResolvedValue(session as never);
|
||||||
|
vi.mocked(canAccess).mockImplementation(
|
||||||
|
(_p: unknown, _s: string, r: number) => r >= 7,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("requireStaff", () => {
|
||||||
|
it("returns staff when authorized", async () => {
|
||||||
|
const s = await requireStaff();
|
||||||
|
expect(s).toEqual({ id: 1, rank: 7, username: "admin" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redirects when unauthorized", async () => {
|
||||||
|
vi.mocked(getAdminContext).mockResolvedValue({
|
||||||
|
session: { user: { id: 2, rank: 1, username: "user" } },
|
||||||
|
permissions: {},
|
||||||
|
} as never);
|
||||||
|
await requireStaff();
|
||||||
|
expect(redirectSafe).toHaveBeenCalledWith("/", "/");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("requirePermission", () => {
|
||||||
|
it("returns staff when both dashboard and specific perm ok", async () => {
|
||||||
|
vi.mocked(canAccess).mockReturnValue(true);
|
||||||
|
expect(await requirePermission("user.edit")).toEqual({
|
||||||
|
id: 1,
|
||||||
|
rank: 7,
|
||||||
|
username: "admin",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("requireMod", () => {
|
||||||
|
it("allows with MOD_DASHBOARD", async () => {
|
||||||
|
vi.mocked(getAdminContext).mockResolvedValue({
|
||||||
|
session: { user: { id: 1, rank: 5, username: "mod" } },
|
||||||
|
permissions: {},
|
||||||
|
} as never);
|
||||||
|
vi.mocked(canAccess).mockImplementation(
|
||||||
|
(_p: unknown, slug: string) => slug === "mod.dashboard",
|
||||||
|
);
|
||||||
|
expect(await requireMod()).toEqual({ id: 1, rank: 5, username: "mod" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redirects without mod perms", async () => {
|
||||||
|
vi.mocked(canAccess).mockReturnValue(false);
|
||||||
|
await requireMod();
|
||||||
|
expect(redirectSafe).toHaveBeenCalledWith("/", "/");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("requireModPermission", () => {
|
||||||
|
it("allows with matching perm", async () => {
|
||||||
|
vi.mocked(canAccess).mockImplementation(
|
||||||
|
(_p: unknown, slug: string) => slug === "mod.tickets.view",
|
||||||
|
);
|
||||||
|
await requireModPermission(["mod.bans.view", "mod.tickets.view"]);
|
||||||
|
expect(redirectSafe).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("requirePermissionRateLimited", () => {
|
||||||
|
it("returns staff when rate limit ok", async () => {
|
||||||
|
vi.mocked(canAccess).mockReturnValue(true);
|
||||||
|
vi.mocked(clientIp).mockResolvedValue("1.2.3.4");
|
||||||
|
vi.mocked(rateLimit).mockResolvedValue({ ok: true });
|
||||||
|
expect(await requirePermissionRateLimited("x")).toEqual({
|
||||||
|
id: 1,
|
||||||
|
rank: 7,
|
||||||
|
username: "admin",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("requireStaffRateLimited", () => {
|
||||||
|
it("redirects on rate limit", async () => {
|
||||||
|
vi.mocked(clientIp).mockResolvedValue("1.2.3.4");
|
||||||
|
vi.mocked(rateLimit).mockResolvedValue({ ok: false });
|
||||||
|
await requireStaffRateLimited();
|
||||||
|
expect(redirectSafe).toHaveBeenCalledWith(
|
||||||
|
"/admin?error=ratelimit",
|
||||||
|
"/admin",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { siteSettings } from "@/lib/services/site-settings";
|
||||||
|
import { getMinStaffRank } from "./min-staff-rank";
|
||||||
|
|
||||||
|
vi.mock("@/lib/services/site-settings", () => ({
|
||||||
|
siteSettings: { get: vi.fn() },
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("getMinStaffRank", () => {
|
||||||
|
it("returns numeric value", async () => {
|
||||||
|
vi.mocked(siteSettings.get).mockResolvedValue("7");
|
||||||
|
expect(await getMinStaffRank()).toBe(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses fallback when setting missing", async () => {
|
||||||
|
vi.mocked(siteSettings.get).mockResolvedValue("");
|
||||||
|
expect(await getMinStaffRank(5)).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("floors values", async () => {
|
||||||
|
vi.mocked(siteSettings.get).mockResolvedValue("7.8");
|
||||||
|
expect(await getMinStaffRank()).toBe(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects zero", async () => {
|
||||||
|
vi.mocked(siteSettings.get).mockResolvedValue("0");
|
||||||
|
expect(await getMinStaffRank(3)).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
personalTokenScope,
|
||||||
|
USER_TOKENABLE_TYPE,
|
||||||
|
} from "./personal-token-scope";
|
||||||
|
|
||||||
|
describe("personal-token-scope", () => {
|
||||||
|
it("exports USER_TOKENABLE_TYPE constant", () => {
|
||||||
|
expect(USER_TOKENABLE_TYPE).toBe("App\\Models\\User");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns correct scope with BigInt", () => {
|
||||||
|
const r = personalTokenScope(42);
|
||||||
|
expect(r.tokenableId).toBe(BigInt(42));
|
||||||
|
expect(r.tokenableType).toBe("App\\Models\\User");
|
||||||
|
});
|
||||||
|
});
|
||||||
+1
-1
@@ -18,7 +18,7 @@ export default defineConfig({
|
|||||||
// Modules under test transitively import @/env; skip its strict parse so
|
// Modules under test transitively import @/env; skip its strict parse so
|
||||||
// unit tests run without a populated .env.
|
// unit tests run without a populated .env.
|
||||||
env: { SKIP_ENV_VALIDATION: "1" },
|
env: { SKIP_ENV_VALIDATION: "1" },
|
||||||
exclude: ["e2e/**", "node_modules/**", ".next/**"],
|
exclude: ["e2e/**", "node_modules/**", ".next/**", ".next.prev/**"],
|
||||||
coverage: {
|
coverage: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
provider: "v8",
|
provider: "v8",
|
||||||
|
|||||||
Reference in new issue
Block a user