lint: fix biome issues in new test files and helpers
CI / check (push) Failing after 34s
CI / preflight (push) Skipped
CI / deploy (push) Skipped

This commit is contained in:
openhands committed 2026-09-21 18:19:39 +02:00
1 parent 99eb3af17b
commit 93862c2275
38 files changed
+423 -249

No files matched your search

+7 -9
View File
@@ -33,12 +33,8 @@ vi.mock("@/lib/services/staff-activity", () => ({
logStaffActivity: mockLogStaffActivity,
}));
import {
createTrack,
deleteTrack,
toggleTrack,
} from "./admin-radio-autodj";
import { RadioAutoDjPlaylist } from "@/lib/db";
import { createTrack, deleteTrack, toggleTrack } from "./admin-radio-autodj";
function form(data: Record<string, string>): FormData {
const fd = new FormData();
@@ -157,8 +153,10 @@ describe("createTrack", () => {
});
it("swallows insert failures but still revalidates", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "insert" ? true : false);
await expect(createTrack(form({ title: "Track" }))).resolves.toBeUndefined();
fakeDbConfig.fail = (kind: string) => kind === "insert";
await expect(
createTrack(form({ title: "Track" })),
).resolves.toBeUndefined();
expect(mockLogStaffActivity).not.toHaveBeenCalled();
expect(mockRevalidatePath).toHaveBeenCalledWith("/admin/radio/autodj");
});
@@ -212,7 +210,7 @@ describe("toggleTrack", () => {
});
it("swallows update failures but still revalidates", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "update" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "update";
await toggleTrack(form({ id: "2", isActive: "on" }));
expect(mockLogStaffActivity).not.toHaveBeenCalled();
expect(mockRevalidatePath).toHaveBeenCalledWith("/admin/radio/autodj");
@@ -246,7 +244,7 @@ describe("deleteTrack", () => {
});
it("swallows delete failures but still revalidates", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "delete" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "delete";
await deleteTrack(form({ id: "4" }));
expect(mockLogStaffActivity).not.toHaveBeenCalled();
expect(mockRevalidatePath).toHaveBeenCalledWith("/admin/radio/autodj");
+14 -13
View File
@@ -35,9 +35,15 @@ vi.mock("@/lib/services/site-settings", () => ({
}));
vi.mock("@/lib/logger", () => ({
logger: { error: mockLoggerError, warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
logger: {
error: mockLoggerError,
warn: vi.fn(),
info: vi.fn(),
debug: vi.fn(),
},
}));
import { RadioBanners, RadioRanks, WebsiteSetting } from "@/lib/db";
import {
createRadioBanner,
createRadioRank,
@@ -48,11 +54,6 @@ import {
updateRadioBanner,
updateRadioRank,
} from "./admin-radio-extra";
import {
RadioBanners,
RadioRanks,
WebsiteSetting,
} from "@/lib/db";
function form(data: Record<string, string>): FormData {
const fd = new FormData();
@@ -158,7 +159,7 @@ describe("saveRadioSettings", () => {
});
it("logs and continues when the bulk upsert fails", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "insert" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "insert";
await saveRadioSettings(form({ __keys: "radio_foo", radio_foo: "1" }));
expect(mockLoggerError).toHaveBeenCalledWith(
"Failed to bulk-save radio settings",
@@ -216,7 +217,7 @@ describe("createRadioBanner", () => {
});
it("logs and continues when the insert fails", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "insert" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "insert";
await createRadioBanner(form({ imagePath: "/banners/c.png" }));
expect(mockLoggerError).toHaveBeenCalledWith(
"Failed to create radio banner",
@@ -266,7 +267,7 @@ describe("updateRadioBanner", () => {
});
it("logs and continues on update failure", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "update" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "update";
await updateRadioBanner(form({ id: "3", imagePath: "/banners/x.png" }));
expect(mockLoggerError).toHaveBeenCalledWith(
"Failed to update radio banner",
@@ -292,7 +293,7 @@ describe("deleteRadioBanner", () => {
});
it("logs and continues on delete failure", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "delete" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "delete";
await deleteRadioBanner(form({ id: "6" }));
expect(mockLoggerError).toHaveBeenCalledWith(
"Failed to delete radio banner",
@@ -339,7 +340,7 @@ describe("createRadioRank", () => {
});
it("logs and continues on insert failure", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "insert" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "insert";
await createRadioRank(form({ name: "Broken" }));
expect(mockLoggerError).toHaveBeenCalledWith(
"Failed to create radio rank",
@@ -383,7 +384,7 @@ describe("updateRadioRank", () => {
});
it("logs and continues on update failure", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "update" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "update";
await updateRadioRank(form({ id: "2", name: "VIP" }));
expect(mockLoggerError).toHaveBeenCalledWith(
"Failed to update radio rank",
@@ -409,7 +410,7 @@ describe("deleteRadioRank", () => {
});
it("logs and continues on delete failure", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "delete" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "delete";
await deleteRadioRank(form({ id: "7" }));
expect(mockLoggerError).toHaveBeenCalledWith(
"Failed to delete radio rank",
+2 -2
View File
@@ -32,8 +32,8 @@ vi.mock("@/lib/services/staff-activity", () => ({
logStaffActivity: mockLogStaffActivity,
}));
import { deleteShout } from "./admin-radio-moderation";
import { RadioShouts } from "@/lib/db";
import { deleteShout } from "./admin-radio-moderation";
function form(data: Record<string, string>): FormData {
const fd = new FormData();
@@ -92,7 +92,7 @@ describe("deleteShout", () => {
});
it("swallows DB failures but still revalidates", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "delete" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "delete";
await deleteShout(form({ id: "3" }));
expect(mockLogStaffActivity).not.toHaveBeenCalled();
expect(mockRevalidatePath).toHaveBeenCalledWith("/admin/radio/moderation");
+4 -3
View File
@@ -42,8 +42,8 @@ vi.mock("@/lib/services/staff-activity", () => ({
logStaffActivity: mockLogStaffActivity,
}));
import { savePoints } from "./admin-radio-points";
import { WebsiteSetting } from "@/lib/db";
import { savePoints } from "./admin-radio-points";
function form(data: Record<string, string>): FormData {
const fd = new FormData();
@@ -62,7 +62,7 @@ beforeEach(() => {
});
});
const POINTS_KEYS = [
const _POINTS_KEYS = [
"radio_points_enabled",
"radio_points_per_minute",
"radio_points_currency",
@@ -165,7 +165,8 @@ describe("savePoints", () => {
});
it("fails soft on DB errors but still revalidates and redirects", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "insert" ? "db down" : false);
fakeDbConfig.fail = (kind: string) =>
kind === "insert" ? "db down" : false;
await savePoints(
form({ radio_points_enabled: "on", radio_points_currency: "diamonds" }),
);
+8 -13
View File
@@ -27,6 +27,7 @@ vi.mock("next/cache", () => ({
unstable_cache: (fn: any) => fn,
}));
import { WebsiteRareValueCategories, WebsiteRareValues } from "@/lib/db";
import {
createCategory,
createValue,
@@ -35,10 +36,6 @@ import {
updateCategory,
updateValue,
} from "./admin-rare-values";
import {
WebsiteRareValueCategories,
WebsiteRareValues,
} from "@/lib/db";
function form(data: Record<string, string>): FormData {
const fd = new FormData();
@@ -86,7 +83,7 @@ describe("createCategory", () => {
});
it("swallows insert errors and still revalidates", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "insert" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "insert";
await createCategory(form({ name: "A", badge: "B" }));
expect(mockRevalidatePath).toHaveBeenCalledWith("/admin/rare-values");
});
@@ -116,7 +113,7 @@ describe("deleteCategory", () => {
});
it("swallows DB errors and still revalidates", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "delete" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "delete";
await deleteCategory(form({ id: "3" }));
expect(mockRevalidatePath).toHaveBeenCalledWith("/admin/rare-values");
});
@@ -152,9 +149,7 @@ describe("createValue", () => {
it("returns early when categoryId, name or furnitureIcon is missing", async () => {
await createValue(form({ name: "X", furnitureIcon: "x.png" }));
await createValue(
form({ categoryId: "1", furnitureIcon: "x.png" }),
);
await createValue(form({ categoryId: "1", furnitureIcon: "x.png" }));
await createValue(form({ categoryId: "1", name: "X" }));
expect(fakeDbConfig.ops).toHaveLength(0);
expect(mockRevalidatePath).not.toHaveBeenCalled();
@@ -179,7 +174,7 @@ describe("createValue", () => {
});
it("swallows insert errors and still revalidates", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "insert" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "insert";
await createValue(
form({ categoryId: "1", name: "X", furnitureIcon: "x.png" }),
);
@@ -203,7 +198,7 @@ describe("deleteValue", () => {
});
it("swallows delete errors and still revalidates", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "delete" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "delete";
await deleteValue(form({ id: "4" }));
expect(mockRevalidatePath).toHaveBeenCalledWith("/admin/rare-values");
});
@@ -231,7 +226,7 @@ describe("updateCategory", () => {
});
it("swallows update errors and still revalidates", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "update" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "update";
await updateCategory(form({ id: "1", name: "X", badge: "B" }));
expect(mockRevalidatePath).toHaveBeenCalledWith("/admin/rare-values");
});
@@ -292,7 +287,7 @@ describe("updateValue", () => {
});
it("swallows update errors and still revalidates", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "update" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "update";
await updateValue(form({ id: "1", name: "X", furnitureIcon: "x.png" }));
expect(mockRevalidatePath).toHaveBeenCalledWith("/admin/rare-values");
});
+24 -5
View File
@@ -53,10 +53,26 @@ describe("updateReferralSettings", () => {
});
expect(mockRequirePermission).toHaveBeenCalledWith("admin.referrals.edit");
expect(mockSiteSettingUpdate).toHaveBeenNthCalledWith(1, "referrals_needed", "5");
expect(mockSiteSettingUpdate).toHaveBeenNthCalledWith(2, "referral_reward_amount", "100");
expect(mockSiteSettingUpdate).toHaveBeenNthCalledWith(3, "referral_reward_currency_type", "duckets");
expect(mockSiteSettingUpdate).toHaveBeenNthCalledWith(4, "referrals_block_same_ip", "1");
expect(mockSiteSettingUpdate).toHaveBeenNthCalledWith(
1,
"referrals_needed",
"5",
);
expect(mockSiteSettingUpdate).toHaveBeenNthCalledWith(
2,
"referral_reward_amount",
"100",
);
expect(mockSiteSettingUpdate).toHaveBeenNthCalledWith(
3,
"referral_reward_currency_type",
"duckets",
);
expect(mockSiteSettingUpdate).toHaveBeenNthCalledWith(
4,
"referrals_block_same_ip",
"1",
);
expect(mockRevalidatePath).toHaveBeenCalledWith("/admin/referrals");
expect(result).toEqual({ ok: true, data: {} });
});
@@ -112,7 +128,10 @@ describe("updateReferralSettings", () => {
rewardCurrency: "credits",
blockSameIp: "0",
});
expect(result).toEqual({ ok: false, error: "Reward amount must be positive" });
expect(result).toEqual({
ok: false,
error: "Reward amount must be positive",
});
});
it("rejects an unsupported reward currency", async () => {
+2 -2
View File
@@ -64,14 +64,14 @@ vi.mock("@/lib/services/habboassets", () => ({
clearBadgeCache: mockClearBadge,
}));
import { WebsiteSetting } from "@/lib/db";
import { PERMS } from "@/lib/permissions";
import {
createSetting,
deleteSetting,
saveManagedSettings,
updateSetting,
} from "./admin-settings";
import { WebsiteSetting } from "@/lib/db";
import { PERMS } from "@/lib/permissions";
function form(data: Record<string, string>): FormData {
const fd = new FormData();
+4 -4
View File
@@ -43,12 +43,12 @@ vi.mock("@/lib/server-log", () => ({
logServerError: mockLogServerError,
}));
import { WebsiteShopArticles } from "@/lib/db";
import {
createShopArticle,
deleteShopArticle,
updateShopArticle,
} from "./admin-shop";
import { WebsiteShopArticles } from "@/lib/db";
function form(data: Record<string, string>): FormData {
const fd = new FormData();
@@ -157,7 +157,7 @@ describe("createShopArticle", () => {
});
it("logs the error and returns without redirecting on insert failure", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "insert" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "insert";
await createShopArticle(form({ name: "Broken" }));
expect(mockLogServerError).toHaveBeenCalledWith(
"admin.shop_create_failed",
@@ -237,7 +237,7 @@ describe("updateShopArticle", () => {
});
it("logs the error and returns without rendering guards on failure", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "update" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "update";
await updateShopArticle(form({ id: "1", name: "Broken" }));
expect(mockLogServerError).toHaveBeenCalledWith(
"admin.shop_update_failed",
@@ -273,7 +273,7 @@ describe("deleteShopArticle", () => {
});
it("logs the error and returns without redirecting on failure", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "delete" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "delete";
await deleteShopArticle(form({ id: "3" }));
expect(mockLogServerError).toHaveBeenCalledWith(
"admin.shop_delete_failed",
+7 -10
View File
@@ -141,7 +141,7 @@ describe("saveTheme", () => {
});
it("fails soft when a write fails and still redirects", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "insert" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "insert";
await saveTheme(form({ color_primary: "#123456" }));
expect(mockReload).not.toHaveBeenCalled();
expect(mockLogStaffActivity).not.toHaveBeenCalled();
@@ -182,7 +182,7 @@ describe("applyPreset", () => {
});
it("fails soft on write errors", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "insert" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "insert";
await applyPreset(form({ preset: "Ocean" }));
expect(mockReload).not.toHaveBeenCalled();
expect(mockLogStaffActivity).not.toHaveBeenCalled();
@@ -195,10 +195,9 @@ describe("saveCustomTheme", () => {
await saveCustomTheme(form({ name: "My Theme" }));
expect(mockSnapshotCurrentTheme).toHaveBeenCalledTimes(1);
expect(mockUpsertCustomTheme).toHaveBeenCalledWith(
"My Theme",
{ color_primary: "#ffffff" },
);
expect(mockUpsertCustomTheme).toHaveBeenCalledWith("My Theme", {
color_primary: "#ffffff",
});
expect(mockLogStaffActivity).toHaveBeenCalledWith({
staffId: 3,
action: "theme_preset",
@@ -254,9 +253,7 @@ describe("applyCustomTheme", () => {
}),
);
expect(mockRevalidatePath).toHaveBeenCalledWith("/", "layout");
expect(mockRedirect).toHaveBeenCalledWith(
"/admin/theme?theme=Studio",
);
expect(mockRedirect).toHaveBeenCalledWith("/admin/theme?theme=Studio");
});
it("redirects when the id is blank", async () => {
@@ -293,7 +290,7 @@ describe("applyCustomTheme", () => {
createdAt: 0,
settings: { color_primary: "#abcdef" },
});
fakeDbConfig.fail = (kind: string) => (kind === "insert" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "insert";
await applyCustomTheme(form({ id: "t1" }));
expect(mockReload).not.toHaveBeenCalled();
expect(mockLogStaffActivity).not.toHaveBeenCalled();
+4 -8
View File
@@ -38,12 +38,8 @@ vi.mock("@/lib/safe-action-shared", () => ({
actionError: (message: string) => ({ ok: false, error: message }),
}));
import {
createVoucher,
deleteVoucher,
updateVoucher,
} from "./admin-vouchers";
import { WebsiteShopVouchers } from "@/lib/db";
import { createVoucher, deleteVoucher, updateVoucher } from "./admin-vouchers";
beforeEach(() => {
vi.clearAllMocks();
@@ -120,7 +116,7 @@ describe("createVoucher", () => {
});
it("returns an error when the insert fails", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "insert" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "insert";
const result = await createVoucher({ code: "X", amount: 1, maxUses: 1 });
expect(mockLogServerError).toHaveBeenCalledWith(
"admin.voucher_create_failed",
@@ -154,7 +150,7 @@ describe("deleteVoucher", () => {
});
it("returns an error when the delete fails", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "delete" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "delete";
const result = await deleteVoucher({ id: "5" });
expect(mockLogServerError).toHaveBeenCalledWith(
"admin.voucher_delete_failed",
@@ -233,7 +229,7 @@ describe("updateVoucher", () => {
});
it("returns an error when the update fails", async () => {
fakeDbConfig.fail = (kind: string) => (kind === "update" ? true : false);
fakeDbConfig.fail = (kind: string) => kind === "update";
const result = await updateVoucher({
id: "9",
code: "X",
+21 -17
View File
@@ -80,7 +80,6 @@ vi.mock("@/lib/db", async () => {
});
import { WebsiteWriteableBoxes } from "@/lib/db";
import { PERMS } from "@/lib/permissions";
import {
createBox,
deleteBox,
@@ -106,9 +105,9 @@ beforeEach(() => {
describe("createBox", () => {
it("rejects callers without PAGES_EDIT", async () => {
mockRequirePermission.mockRejectedValueOnce(new Error("denied"));
await expect(createBox(fakeForm({ title: "Hi" }) as FormData)).rejects.toThrow(
"denied",
);
await expect(
createBox(fakeForm({ title: "Hi" }) as FormData),
).rejects.toThrow("denied");
expect(state.insertCalls).toHaveLength(0);
});
@@ -166,15 +165,12 @@ describe("createBox", () => {
isActive: "0",
}) as FormData,
);
expect(
(state.insertCalls.at(-1)?.values as Record<string, unknown>).position,
).toBe(expected);
expect(
(state.insertCalls.at(-1)?.values as Record<string, unknown>).isActive,
).toBe(false);
expect(
(state.insertCalls.at(-1)?.values as Record<string, unknown>).icon,
).toBeNull();
const lastInsert = state.insertCalls.at(-1);
expect(lastInsert).toBeDefined();
const vals = lastInsert?.values as Record<string, unknown>;
expect(vals.position).toBe(expected);
expect(vals.isActive).toBe(false);
expect(vals.icon).toBeNull();
}
});
@@ -267,7 +263,9 @@ describe("deleteBox", () => {
it("swallows a failed delete", async () => {
state.deleteError = new Error("db down");
await expect(deleteBox(fakeForm({ id: "1" }) as FormData)).resolves.toBeUndefined();
await expect(
deleteBox(fakeForm({ id: "1" }) as FormData),
).resolves.toBeUndefined();
expect(mockLogStaffActivity).not.toHaveBeenCalled();
expect(mockRevalidatePath).not.toHaveBeenCalled();
});
@@ -282,7 +280,9 @@ describe("toggleBox", () => {
it("activates when next is 1", async () => {
await toggleBox(fakeForm({ id: "2", next: "1" }) as FormData);
expect(state.updateCalls).toHaveLength(1);
expect((state.updateCalls[0].values as Record<string, unknown>).isActive).toBe(true);
expect(
(state.updateCalls[0].values as Record<string, unknown>).isActive,
).toBe(true);
expect(mockLogStaffActivity).toHaveBeenCalledWith(
expect.objectContaining({
action: "writeable_box_toggle",
@@ -294,7 +294,9 @@ describe("toggleBox", () => {
it("hides when next is anything but 1", async () => {
await toggleBox(fakeForm({ id: "2", next: "0" }) as FormData);
expect((state.updateCalls[0].values as Record<string, unknown>).isActive).toBe(false);
expect(
(state.updateCalls[0].values as Record<string, unknown>).isActive,
).toBe(false);
expect(mockLogStaffActivity).toHaveBeenCalledWith(
expect.objectContaining({
description: "Hid writeable box #2",
@@ -304,7 +306,9 @@ describe("toggleBox", () => {
it("swallows a failed toggle", async () => {
state.updateError = new Error("db down");
await expect(toggleBox(fakeForm({ id: "2", next: "1" }) as FormData)).resolves.toBeUndefined();
await expect(
toggleBox(fakeForm({ id: "2", next: "1" }) as FormData),
).resolves.toBeUndefined();
expect(mockLogStaffActivity).not.toHaveBeenCalled();
expect(mockRevalidatePath).not.toHaveBeenCalled();
});
+1 -2
View File
@@ -46,8 +46,7 @@ vi.mock("@/lib/db", async () => {
throw e;
}
if (table === schema.WebsiteArticles) return state.articles;
if (table === schema.WebsiteArticleReactions)
return state.reactions;
if (table === schema.WebsiteArticleReactions) return state.reactions;
return [];
}),
update: (table: unknown) => ({
+4 -6
View File
@@ -75,11 +75,7 @@ vi.mock("@/lib/db", async () => {
import { WebsiteBanner } from "@/lib/db";
import { PERMS } from "@/lib/permissions";
import {
createBanner,
deleteBanner,
updateBanner,
} from "./banners";
import { createBanner, deleteBanner, updateBanner } from "./banners";
function session() {
return {
@@ -172,7 +168,9 @@ describe("createBanner", () => {
const result = await createBanner({ image: "i" });
expect(result.ok).toBe(false);
expect(result.error).toBe("Validation failed");
expect((result as { fieldErrors: Record<string, string[]> }).fieldErrors.title).toBeDefined();
expect(
(result as { fieldErrors: Record<string, string[]> }).fieldErrors.title,
).toBeDefined();
});
it("rejects an out-of-range isActive value", async () => {
+10 -4
View File
@@ -1,6 +1,5 @@
import { eq } from "drizzle-orm";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fakeForm } from "@/test/fake-form";
const state = vi.hoisted(() => ({
deletes: [] as { table: unknown; where: unknown }[],
@@ -87,7 +86,6 @@ vi.mock("@/lib/db", async () => {
});
import { CatalogItemsBc } from "@/lib/db";
import { PERMS } from "@/lib/permissions";
import {
createBcItem,
createBcPage,
@@ -110,7 +108,10 @@ beforeEach(() => {
mockWithCatalogExport.mockImplementation(
async (fn: () => unknown) => await fn(),
);
mockCatalogFailure.mockReturnValue({ message: "Catalog op failed", status: 400 });
mockCatalogFailure.mockReturnValue({
message: "Catalog op failed",
status: 400,
});
mockCreateBcOfferCommand.mockResolvedValue(501);
mockCreatePageCommand.mockResolvedValue(201);
mockSendCatalogUpdate.mockResolvedValue({});
@@ -265,7 +266,12 @@ describe("toggleBcPage", () => {
it("toggles the requested field", async () => {
const result = await toggleBcPage({ id: 2, field: "visible" });
expect(result).toEqual({ ok: true });
expect(mockTogglePageCommand).toHaveBeenCalledWith("bc", 2, "visible", staff.id);
expect(mockTogglePageCommand).toHaveBeenCalledWith(
"bc",
2,
"visible",
staff.id,
);
expect(mockSendCatalogUpdate).toHaveBeenCalled();
expect(mockRevalidatePath).toHaveBeenCalledWith(
"/admin/catalog/builder-club",
+5 -4
View File
@@ -102,7 +102,6 @@ vi.mock("@/lib/db", async () => {
};
});
import { PERMS } from "@/lib/permissions";
import {
alertUser,
disconnectUser,
@@ -113,9 +112,9 @@ import {
giveDiamonds,
giveDuckets,
hotelAlert,
sendGift,
setMotto,
setRank,
sendGift,
updateCatalog,
updateNavigator,
updateWordFilter,
@@ -297,10 +296,12 @@ describe("giveCredits / giveDuckets / giveDiamonds", () => {
});
it("rejects negative amounts", async () => {
await expect(giveCredits({ userId: 1, credits: 0 })).resolves.toMatchObject({
await expect(giveCredits({ userId: 1, credits: 0 })).resolves.toMatchObject(
{
ok: false,
error: "Validation failed",
});
},
);
});
});
+6 -1
View File
@@ -33,7 +33,12 @@ vi.mock("@/lib/auth", () => ({ auth: mockAuth }));
const mockRateLimit = vi.hoisted(() => vi.fn());
vi.mock("@/lib/rate-limit", () => ({ rateLimit: mockRateLimit }));
const mockLogger = vi.hoisted(() => ({ error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() }));
const mockLogger = vi.hoisted(() => ({
error: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
debug: vi.fn(),
}));
vi.mock("@/lib/logger", () => ({ logger: mockLogger }));
vi.mock("@/lib/services/daily-rewards", () => ({
+4 -1
View File
@@ -258,7 +258,10 @@ describe("buyBadge", () => {
});
expect(mockGiveBadge).toHaveBeenCalledWith(7, "MYBADGE");
expect(mockRevalidatePath).toHaveBeenCalledWith("/draw-badge");
console.log("DEBUG caught:", state.caughtErrors.map((e) => (e as Error).message ?? e));
console.log(
"DEBUG caught:",
state.caughtErrors.map((e) => (e as Error).message ?? e),
);
});
it("updates an existing badge slot instead of creating a new one", async () => {
+1 -5
View File
@@ -69,11 +69,7 @@ describe("applyDj", () => {
await expect(applyDj(buildForm())).rejects.toThrow(
"/radio/apply?submitted=1",
);
expect(state.rateLimit).toHaveBeenCalledWith(
"radio-apply:5",
2,
300_000,
);
expect(state.rateLimit).toHaveBeenCalledWith("radio-apply:5", 2, 300_000);
expect(state.insert).toHaveBeenCalledOnce();
expect(state.insert.mock.calls[0][0]).toBe(RadioApplications);
expect(state.insert.mock.calls[0][1]).toMatchObject({
+3 -3
View File
@@ -80,9 +80,9 @@ describe("submitRequest", () => {
});
it("stores null for whichever of song/artist is left empty", async () => {
await expect(
submitRequest(buildForm({ artist: "" })),
).rejects.toThrow("/radio/requests?posted=1");
await expect(submitRequest(buildForm({ artist: "" }))).rejects.toThrow(
"/radio/requests?posted=1",
);
expect(state.insert.mock.calls[0][1]).toMatchObject({
songTitle: "Never Gonna Give You Up",
artist: null,
+12 -10
View File
@@ -93,21 +93,23 @@ describe("postShout", () => {
});
it("rejects an empty or whitespace-only message as invalid", async () => {
await expect(
postShout(buildForm({ message: "" })),
).rejects.toThrow("/radio/shouts?error=invalid");
await expect(
postShout(buildForm({ message: " " })),
).rejects.toThrow("/radio/shouts?error=invalid");
await expect(
postShout(buildForm({ message: undefined })),
).rejects.toThrow("/radio/shouts?error=invalid");
await expect(postShout(buildForm({ message: "" }))).rejects.toThrow(
"/radio/shouts?error=invalid",
);
await expect(postShout(buildForm({ message: " " }))).rejects.toThrow(
"/radio/shouts?error=invalid",
);
await expect(postShout(buildForm({ message: undefined }))).rejects.toThrow(
"/radio/shouts?error=invalid",
);
expect(state.insert).not.toHaveBeenCalled();
expect(state.revalidatePath).toHaveBeenCalledWith("/radio/shouts");
});
it("redirects with error=moderated when content moderation rejects the message", async () => {
state.moderateOrThrow.mockRejectedValue(new Error("Blocked by word filter"));
state.moderateOrThrow.mockRejectedValue(
new Error("Blocked by word filter"),
);
await expect(postShout(buildForm())).rejects.toThrow(
"/radio/shouts?error=moderated",
);
+5 -5
View File
@@ -35,7 +35,8 @@ vi.mock("@/lib/services/rcon", () => ({
vi.mock("@/lib/db", async () => {
const schema = await import("@/db/schema");
const { createFakeDb } = await import("@/test/fake-db");
const fake = createFakeDb((table: unknown, projection: Record<string, unknown>) => {
const fake = createFakeDb(
(_table: unknown, projection: Record<string, unknown>) => {
if ("referralsTotal" in projection) {
if (state.referralsFail) throw new Error("referrals down");
return state.referralsRows;
@@ -45,7 +46,8 @@ vi.mock("@/lib/db", async () => {
return state.settingsRows;
}
return [];
});
},
);
return {
...schema,
db: {
@@ -218,9 +220,7 @@ describe("claimReferral", () => {
});
it("rejects a non-positive or garbled reward amount as bad_config", async () => {
state.settingsRows = [
{ key: "referral_reward_amount", value: "abc" },
];
state.settingsRows = [{ key: "referral_reward_amount", value: "abc" }];
await expect(claimReferral(new FormData())).rejects.toThrow(
"/me?error=bad_config",
);
+18 -7
View File
@@ -59,14 +59,16 @@ vi.mock("@/lib/services/site-settings", () => ({
vi.mock("@/lib/db", async () => {
const schema = await import("@/db/schema");
const { createFakeDb } = await import("@/test/fake-db");
const fake = createFakeDb((table: unknown, projection: Record<string, unknown>) => {
const fake = createFakeDb(
(_table: unknown, projection: Record<string, unknown>) => {
if ("total" in projection) {
if (state.failCount) return Promise.reject(new Error("count down"));
return [{ total: state.countTotal }];
}
if (state.failUsernameCheck) throw new Error("check down");
return state.userRows;
});
},
);
return {
...schema,
db: {
@@ -163,14 +165,14 @@ describe("register", () => {
expect(values.username).toBe("Bob_88");
expect(values.mail).toBe("[email protected]");
expect(state.after).toHaveBeenCalledOnce();
await state.afterCb!();
await state.afterCb?.();
expect(state.sendVerification).toHaveBeenCalledWith("[email protected]");
});
it("logs a warning when the verification email fails to send", async () => {
state.sendVerification.mockRejectedValue(new Error("smtp down"));
await register(PREV, buildForm({ mail: "[email protected]" }));
await state.afterCb!();
await state.afterCb?.();
expect(state.logger.warn).toHaveBeenCalledWith(
"Failed to send verification email after registration",
);
@@ -219,7 +221,10 @@ describe("register", () => {
it("rejects passwords without a digit", async () => {
const result = await register(
PREV,
buildForm({ password: "Secretsecret", password_confirmation: "Secretsecret" }),
buildForm({
password: "Secretsecret",
password_confirmation: "Secretsecret",
}),
);
expect(result.error).toContain("digit");
});
@@ -317,7 +322,10 @@ describe("register", () => {
it("rejects an already-taken username", async () => {
state.userRows = [{ id: 7 }];
const result = await runValidRegistration();
expect(result).toEqual({ error: "That username is already taken", ok: false });
expect(result).toEqual({
error: "That username is already taken",
ok: false,
});
expect(state.insert).not.toHaveBeenCalled();
});
@@ -339,7 +347,10 @@ describe("register", () => {
cause: { code: "ER_DUP_ENTRY" },
});
const result = await runValidRegistration();
expect(result).toEqual({ error: "That username is already taken", ok: false });
expect(result).toEqual({
error: "That username is already taken",
ok: false,
});
expect(state.logger.error).not.toHaveBeenCalled();
});
+15 -7
View File
@@ -1,7 +1,11 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const state = vi.hoisted(() => ({
requirePermission: vi.fn(async () => ({ id: 100, rank: 7, username: "staff" })),
requirePermission: vi.fn(async () => ({
id: 100,
rank: 7,
username: "staff",
})),
revalidatePath: vi.fn(),
del: vi.fn(async () => [{ affectedRows: 1 }]),
update: vi.fn(async () => [{ affectedRows: 1 }]),
@@ -27,10 +31,12 @@ vi.mock("@/lib/services/rcon", () => ({ rcon: { send: state.rconSend } }));
vi.mock("@/lib/db", async () => {
const schema = await import("@/db/schema");
const { createFakeDb } = await import("@/test/fake-db");
const fake = createFakeDb((table: unknown, projection: Record<string, unknown>) => {
const fake = createFakeDb(
(_table: unknown, _projection: Record<string, unknown>) => {
if (state.denied) throw new Error("not allowed");
return state.roomRows;
});
},
);
return {
...schema,
db: {
@@ -92,9 +98,9 @@ describe("rooms actions", () => {
it("denies room edits without permission", async () => {
state.requirePermission.mockRejectedValueOnce(new Error("forbidden"));
await expect(
updateRoomItem({ roomId: 9, itemId: 4 }),
).rejects.toThrow("forbidden");
await expect(updateRoomItem({ roomId: 9, itemId: 4 })).rejects.toThrow(
"forbidden",
);
expect(state.update).not.toHaveBeenCalled();
});
@@ -114,7 +120,9 @@ describe("rooms actions", () => {
await bulkDeleteRoomItems({ roomId: 9, itemIds: [] });
expect(state.del).toHaveBeenCalledWith(Items, expect.anything());
expect(state.logStaffActivity).toHaveBeenCalledWith(
expect.objectContaining({ description: "Deleted 0 item(s) from room #9" }),
expect.objectContaining({
description: "Deleted 0 item(s) from room #9",
}),
);
expect(state.revalidatePath).toHaveBeenCalledWith("/admin/rooms/9/furni");
});
+24 -13
View File
@@ -49,7 +49,8 @@ vi.mock("@/lib/server-log", () => ({
vi.mock("@/lib/db", async () => {
const schema = await import("@/db/schema");
const { createFakeDb } = await import("@/test/fake-db");
const fake = createFakeDb((table: unknown, projection: Record<string, unknown>) => {
const fake = createFakeDb(
(table: unknown, _projection: Record<string, unknown>) => {
if (table === schema.UsersBadges) {
if (state.badgeQueue.length) return state.badgeQueue.shift() as RowList;
return state.badgeRows;
@@ -57,7 +58,8 @@ vi.mock("@/lib/db", async () => {
if (table === schema.User) return state.userRows;
if (table === schema.WebsiteShopArticles) return state.articleRows;
return [];
});
},
);
const db = {
...fake,
update: (table: unknown) => ({
@@ -116,8 +118,7 @@ describe("buyShopArticle", () => {
state.badgeQueue = [];
state.isBadge = false;
state.transaction.mockImplementation(
async (fn: (tx: unknown) => Promise<unknown>, txDb: unknown) =>
fn(txDb),
async (fn: (tx: unknown) => Promise<unknown>, txDb: unknown) => fn(txDb),
);
});
@@ -127,7 +128,11 @@ describe("buyShopArticle", () => {
);
expect(state.rateLimit).toHaveBeenCalledWith("shop-buy:5", 5, 60_000);
expect(state.transaction).toHaveBeenCalled();
expect(state.update).toHaveBeenCalledWith(User, expect.anything(), expect.anything());
expect(state.update).toHaveBeenCalledWith(
User,
expect.anything(),
expect.anything(),
);
expect(state.insert).toHaveBeenCalledTimes(2);
expect(state.insert.mock.calls[0][0]).toBe(UsersBadges);
expect(state.insert.mock.calls[0][1]).toEqual({
@@ -164,9 +169,9 @@ describe("buyShopArticle", () => {
});
it("omits the category query param when it is not numeric", async () => {
await expect(buyShopArticle(shopForm({ categoryId: "abc" }))).rejects.toThrow(
"/shop?bought=1&package=StarterPack",
);
await expect(
buyShopArticle(shopForm({ categoryId: "abc" })),
).rejects.toThrow("/shop?bought=1&package=StarterPack");
});
it("raises the buyer rank when the package grants a higher rank", async () => {
@@ -213,7 +218,10 @@ describe("buyShopArticle", () => {
it("ignores badge codes longer than 32 characters or whitespace", async () => {
state.articleRows = [
{ ...ARTICLE, badges: "OK,, , VERYLONG_badge_code_that_exceeds_32_chars" },
{
...ARTICLE,
badges: "OK,, , VERYLONG_badge_code_that_exceeds_32_chars",
},
];
await expect(buyShopArticle(shopForm())).rejects.toThrow(
"/shop?category=1&bought=1",
@@ -284,9 +292,9 @@ describe("buyShopArticle", () => {
await expect(
buyShopArticle(shopForm({ articleId: "t-shirt" })),
).rejects.toThrow("/shop?category=1&error=invalid");
await expect(buyShopArticle(shopForm({ articleId: "4.5" }))).rejects.toThrow(
"/shop?category=1&error=invalid",
);
await expect(
buyShopArticle(shopForm({ articleId: "4.5" })),
).rejects.toThrow("/shop?category=1&error=invalid");
expect(state.transaction).not.toHaveBeenCalled();
});
@@ -335,7 +343,10 @@ describe("buyShopArticle", () => {
await expect(buyShopArticle(shopForm())).rejects.toThrow(
"/shop?category=1&bought=1",
);
const updateCall = state.update.mock.calls[0] as [unknown, Record<string, unknown>];
const updateCall = state.update.mock.calls[0] as [
unknown,
Record<string, unknown>,
];
expect(updateCall[1]).toHaveProperty("credits");
});
});
+14 -17
View File
@@ -30,7 +30,8 @@ vi.mock("@/lib/db", async () => {
const schema = await import("@/db/schema");
const { createFakeDb } = await import("@/test/fake-db");
const fake = createFakeDb(() => {
if (state.selectQueue.length) return state.selectQueue.shift() as Queue[number];
if (state.selectQueue.length)
return state.selectQueue.shift() as Queue[number];
return state.rows;
});
const db = {
@@ -121,9 +122,9 @@ describe("sendFriendRequest", () => {
});
it("redirects to the root path when no username is supplied", async () => {
await expect(sendFriendRequest(friendForm({ username: "" }))).rejects.toThrow(
"/?friend=sent",
);
await expect(
sendFriendRequest(friendForm({ username: "" })),
).rejects.toThrow("/?friend=sent");
expect(state.revalidatePath).toHaveBeenCalledWith("/messages");
});
@@ -131,9 +132,9 @@ describe("sendFriendRequest", () => {
await expect(
sendFriendRequest(friendForm({ userId: "abc" })),
).rejects.toThrow("/u/Bob?error=invalid");
await expect(sendFriendRequest(friendForm({ userId: "0" }))).rejects.toThrow(
"/u/Bob?error=invalid",
);
await expect(
sendFriendRequest(friendForm({ userId: "0" })),
).rejects.toThrow("/u/Bob?error=invalid");
await expect(
sendFriendRequest(friendForm({ userId: "5.5" })),
).rejects.toThrow("/u/Bob?error=invalid");
@@ -144,9 +145,9 @@ describe("sendFriendRequest", () => {
});
it("blocks sending a request to yourself", async () => {
await expect(sendFriendRequest(friendForm({ userId: "5" }))).rejects.toThrow(
"/u/Bob?error=self",
);
await expect(
sendFriendRequest(friendForm({ userId: "5" })),
).rejects.toThrow("/u/Bob?error=self");
expect(state.insert).not.toHaveBeenCalled();
});
@@ -209,8 +210,7 @@ describe("postThread", () => {
state.selectQueue = [];
state.rows = [];
state.transaction.mockImplementation(
async (fn: (tx: unknown) => Promise<unknown>, txDb: unknown) =>
fn(txDb),
async (fn: (tx: unknown) => Promise<unknown>, txDb: unknown) => fn(txDb),
);
});
@@ -324,8 +324,7 @@ describe("replyToThread", () => {
state.selectQueue = [];
state.rows = [];
state.transaction.mockImplementation(
async (fn: (tx: unknown) => Promise<unknown>, txDb: unknown) =>
fn(txDb),
async (fn: (tx: unknown) => Promise<unknown>, txDb: unknown) => fn(txDb),
);
});
@@ -350,9 +349,7 @@ describe("replyToThread", () => {
updatedAt: expect.any(Number),
});
expect(state.revalidatePath).toHaveBeenCalledWith("/guilds/10/forum");
expect(state.revalidatePath).toHaveBeenCalledWith(
"/guilds/10/forum/20",
);
expect(state.revalidatePath).toHaveBeenCalledWith("/guilds/10/forum/20");
});
it("floors a null posts_count at one", async () => {
+12 -2
View File
@@ -1,7 +1,11 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const state = vi.hoisted(() => ({
requirePermission: vi.fn(async () => ({ id: 100, rank: 7, username: "staff" })),
requirePermission: vi.fn(async () => ({
id: 100,
rank: 7,
username: "staff",
})),
revalidatePath: vi.fn(),
del: vi.fn(async () => [{ affectedRows: 1 }]),
update: vi.fn(async () => [{ affectedRows: 1 }]),
@@ -82,7 +86,13 @@ describe("soundtrack actions", () => {
it("denies updates without the catalog edit permission", async () => {
state.requirePermission.mockRejectedValueOnce(new Error("forbidden"));
await expect(
updateSoundtrack({ id: 12, name: "x", author: "y", track: "z", length: 1 }),
updateSoundtrack({
id: 12,
name: "x",
author: "y",
track: "z",
length: 1,
}),
).rejects.toThrow("forbidden");
expect(state.update).not.toHaveBeenCalled();
});
+12 -3
View File
@@ -3,14 +3,19 @@ import {
createMockAuth,
createMockDb,
createMockLogStaffActivity,
createMockRevalidatePath,
createMockRcon,
createMockRevalidatePath,
} from "./test-helpers";
describe("test-helpers", () => {
it("createMockDb returns vi.fn-backed query methods", () => {
const db = createMockDb();
expect(Object.keys(db).sort()).toEqual(["delete", "insert", "select", "update"]);
expect(Object.keys(db).sort()).toEqual([
"delete",
"insert",
"select",
"update",
]);
for (const method of ["insert", "update", "delete", "select"]) {
expect(db[method as keyof typeof db]).toBeTypeOf("function");
expect(vi.isMockFunction(db[method as keyof typeof db])).toBe(true);
@@ -19,7 +24,11 @@ describe("test-helpers", () => {
it("createMockAuth returns next-auth-like object", () => {
const auth = createMockAuth();
expect(auth).toEqual({ auth: expect.any(Function), handlers: {}, signOut: expect.any(Function) });
expect(auth).toEqual({
auth: expect.any(Function),
handlers: {},
signOut: expect.any(Function),
});
expect(vi.isMockFunction(auth.auth)).toBe(true);
expect(vi.isMockFunction(auth.signOut)).toBe(true);
});
+12 -7
View File
@@ -20,7 +20,10 @@ const actionErrorClass = vi.hoisted(
);
vi.mock("@/lib/safe-action", () => ({
adminAction: (opts: { permission?: string | readonly string[]; schema?: z.ZodType }, handler: (ctx: any) => unknown) => {
adminAction: (
opts: { permission?: string | readonly string[]; schema?: z.ZodType },
handler: (ctx: any) => unknown,
) => {
return async (input: unknown) => {
const needed = Array.isArray(opts.permission)
? opts.permission
@@ -140,12 +143,14 @@ describe("createTemplate", () => {
});
it("rejects empty titles and content", async () => {
expect(
await createTemplate({ title: "", content: "x" }),
).toMatchObject({ ok: false, error: "Validation failed" });
expect(
await createTemplate({ title: "T", content: "" }),
).toMatchObject({ ok: false, error: "Validation failed" });
expect(await createTemplate({ title: "", content: "x" })).toMatchObject({
ok: false,
error: "Validation failed",
});
expect(await createTemplate({ title: "T", content: "" })).toMatchObject({
ok: false,
error: "Validation failed",
});
});
it("requires the tickets edit permission", async () => {
+43 -12
View File
@@ -13,7 +13,12 @@ const actionErrorClass = vi.hoisted(
const state = vi.hoisted(() => ({
allowed: [] as string[],
tickets: [] as { id: number; assigneeId: number | null; status: string; priority: string }[],
tickets: [] as {
id: number;
assigneeId: number | null;
status: string;
priority: string;
}[],
insertCall: undefined as unknown,
updateCall: undefined as unknown,
rowsForSelect: [] as unknown[],
@@ -21,7 +26,10 @@ const state = vi.hoisted(() => ({
}));
vi.mock("@/lib/safe-action", () => ({
adminAction: (opts: { permission?: string | readonly string[]; schema?: z.ZodType }, handler: (ctx: any) => unknown) => {
adminAction: (
opts: { permission?: string | readonly string[]; schema?: z.ZodType },
handler: (ctx: any) => unknown,
) => {
return async (input: unknown) => {
const needed = Array.isArray(opts.permission)
? opts.permission
@@ -63,7 +71,10 @@ vi.mock("@/lib/safe-action-shared", () => ({
}));
vi.mock("@/lib/permissions", () => ({
PERMS: { TICKETS_EDIT: "admin.tickets.edit", MOD_TICKETS_EDIT: "mod.tickets.edit" },
PERMS: {
TICKETS_EDIT: "admin.tickets.edit",
MOD_TICKETS_EDIT: "mod.tickets.edit",
},
}));
vi.mock("@/lib/services/audit", () => ({ logAudit: vi.fn() }));
@@ -135,8 +146,13 @@ describe("adminReplyTicket", () => {
});
it("inserts a staff message and auto-assigns an unassigned ticket", async () => {
state.tickets = [{ id: 1, assigneeId: null, status: "open", priority: "low" }];
const res = await adminReplyTicket({ ticketId: 1, message: "Working on it" });
state.tickets = [
{ id: 1, assigneeId: null, status: "open", priority: "low" },
];
const res = await adminReplyTicket({
ticketId: 1,
message: "Working on it",
});
expect(res).toEqual({ ok: true, data: {} });
expect(state.insertCall.values).toMatchObject({
ticketId: 1,
@@ -154,7 +170,9 @@ describe("adminReplyTicket", () => {
});
it("does not clobber an existing assignee", async () => {
state.tickets = [{ id: 2, assigneeId: 55, status: "open", priority: "low" }];
state.tickets = [
{ id: 2, assigneeId: 55, status: "open", priority: "low" },
];
await adminReplyTicket({ ticketId: 2, message: "Already owned" });
expect(state.updateCall.values).toMatchObject({ status: "waiting" });
expect(state.updateCall.values.assigneeId).toBeUndefined();
@@ -163,7 +181,9 @@ describe("adminReplyTicket", () => {
describe("updateTicketStatus", () => {
it("sets closedAt when closing a ticket", async () => {
state.tickets = [{ id: 3, assigneeId: 55, status: "waiting", priority: "low" }];
state.tickets = [
{ id: 3, assigneeId: 55, status: "waiting", priority: "low" },
];
const res = await updateTicketStatus({ ticketId: 3, status: "closed" });
expect(res).toEqual({ ok: true, data: {} });
expect(state.updateCall.values).toMatchObject({ status: "closed" });
@@ -178,7 +198,9 @@ describe("updateTicketStatus", () => {
});
it("auto-assigns when moving an unowned ticket to in_progress", async () => {
state.tickets = [{ id: 4, assigneeId: null, status: "open", priority: "low" }];
state.tickets = [
{ id: 4, assigneeId: null, status: "open", priority: "low" },
];
await updateTicketStatus({ ticketId: 4, status: "in_progress" });
expect(state.updateCall.values).toMatchObject({
status: "in_progress",
@@ -203,7 +225,9 @@ describe("updateTicketStatus", () => {
describe("assignTicket", () => {
it("marks an assigned ticket in_progress", async () => {
state.tickets = [{ id: 6, assigneeId: null, status: "open", priority: "low" }];
state.tickets = [
{ id: 6, assigneeId: null, status: "open", priority: "low" },
];
const res = await assignTicket({ ticketId: 6, assigneeId: 42 });
expect(res).toEqual({ ok: true, data: {} });
expect(state.updateCall.values).toMatchObject({
@@ -220,7 +244,9 @@ describe("assignTicket", () => {
});
it("returns the ticket to open when unassigning", async () => {
state.tickets = [{ id: 7, assigneeId: 42, status: "in_progress", priority: "low" }];
state.tickets = [
{ id: 7, assigneeId: 42, status: "in_progress", priority: "low" },
];
await assignTicket({ ticketId: 7, assigneeId: null });
expect(state.updateCall.values).toMatchObject({
assigneeId: null,
@@ -254,14 +280,19 @@ describe("updateTicketPriority", () => {
it("throws when the ticket does not exist", async () => {
state.tickets = [];
expect(await updateTicketPriority({ ticketId: 1, priority: "high" })).toEqual({
expect(
await updateTicketPriority({ ticketId: 1, priority: "high" }),
).toEqual({
ok: false,
error: "Ticket not found",
});
});
it("rejects an unknown priority value", async () => {
const res = await updateTicketPriority({ ticketId: 1, priority: "max" as never });
const res = await updateTicketPriority({
ticketId: 1,
priority: "max" as never,
});
expect(res).toMatchObject({ ok: false, error: "Validation failed" });
});
});
+57 -17
View File
@@ -3,9 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const state = vi.hoisted(() => ({
allowed: [] as string[],
saveCmsTranslation: vi.fn(),
translationError: vi.fn<
(code: string, args?: { key?: string }) => string
>(),
translationError: vi.fn<(code: string, args?: { key?: string }) => string>(),
patchJson5: vi.fn(),
readFile: vi.fn(),
writeFile: vi.fn(),
@@ -81,7 +79,11 @@ vi.mock("node:fs/promises", () => ({
readFile: state.readFile,
writeFile: state.writeFile,
mkdir: vi.fn(),
default: { readFile: state.readFile, writeFile: state.writeFile, mkdir: vi.fn() },
default: {
readFile: state.readFile,
writeFile: state.writeFile,
mkdir: vi.fn(),
},
}));
vi.mock("@/lib/cms-translations", () => ({
@@ -97,10 +99,13 @@ const mockGetClientTranslationFile = vi.hoisted(() => vi.fn());
vi.mock("@/lib/client-translation-files", async (importOriginal) => {
const original =
await importOriginal<typeof import("@/lib/client-translation-files")>();
return { ...original, getClientTranslationFile: mockGetClientTranslationFile };
return {
...original,
getClientTranslationFile: mockGetClientTranslationFile,
};
});
import { CLIENT_TRANSLATION_FILES, getClientTranslationFile } from "@/lib/client-translation-files";
import { CLIENT_TRANSLATION_FILES } from "@/lib/client-translation-files";
import { saveClientTranslations, saveTranslations } from "./translations";
const snapshot = { messages: { "a.b": "x" }, revision: "ab".repeat(32) };
@@ -173,7 +178,11 @@ describe("saveTranslations", () => {
it("rejects an unsupported locale and a malformed revision", async () => {
expect(
await saveTranslations({ locale: "xx" as never, revision: "a".repeat(64), changes: {} }),
await saveTranslations({
locale: "xx" as never,
revision: "a".repeat(64),
changes: {},
}),
).toMatchObject({ ok: false, error: "Validation failed" });
expect(
await saveTranslations({ locale: "en", revision: "short", changes: {} }),
@@ -183,7 +192,11 @@ describe("saveTranslations", () => {
it("requires the settings edit permission", async () => {
state.allowed = [];
expect(
await saveTranslations({ locale: "en", revision: "a".repeat(64), changes: {} }),
await saveTranslations({
locale: "en",
revision: "a".repeat(64),
changes: {},
}),
).toEqual({ ok: false, error: "Unauthorized" });
});
});
@@ -191,12 +204,18 @@ describe("saveTranslations", () => {
describe("saveClientTranslations", () => {
it("reports an unknown file id", async () => {
mockGetClientTranslationFile.mockReturnValueOnce(undefined);
const res = await saveClientTranslations({ fileId: "badge-texts-en", data: {} });
const res = await saveClientTranslations({
fileId: "badge-texts-en",
data: {},
});
expect(res).toEqual({ ok: false, error: "Unknown file" });
});
it("refuses to write read-only files", async () => {
const res = await saveClientTranslations({ fileId: "ui-texts-en", data: {} });
const res = await saveClientTranslations({
fileId: "ui-texts-en",
data: {},
});
expect(res).toEqual({ ok: false, error: "File is read-only" });
expect(state.writeFile).not.toHaveBeenCalled();
});
@@ -207,7 +226,10 @@ describe("saveClientTranslations", () => {
fileId: "badge-texts-en",
data: { x: "y" },
});
expect(res).toEqual({ ok: true, data: { commentsLost: false, unpatchedKeys: [] } });
expect(res).toEqual({
ok: true,
data: { commentsLost: false, unpatchedKeys: [] },
});
expect(state.writeFile).toHaveBeenCalledWith(
expect.stringContaining("badge-texts-en.json"),
JSON.stringify({ x: "y" }, null, 4),
@@ -217,12 +239,18 @@ describe("saveClientTranslations", () => {
it("writes a surgical JSON5 patch when every key is patchable", async () => {
state.readFile.mockResolvedValue('{"greeting": "hi",}');
state.patchJson5.mockReturnValue({ content: '{"greeting": "ciao",}', unpatchedKeys: [] });
state.patchJson5.mockReturnValue({
content: '{"greeting": "ciao",}',
unpatchedKeys: [],
});
const res = await saveClientTranslations({
fileId: "ui-texts-it",
data: { greeting: "ciao" },
});
expect(res).toEqual({ ok: true, data: { commentsLost: false, unpatchedKeys: [] } });
expect(res).toEqual({
ok: true,
data: { commentsLost: false, unpatchedKeys: [] },
});
expect(state.patchJson5).toHaveBeenCalledWith(
'{"greeting": "hi",}',
{ greeting: "hi" },
@@ -237,7 +265,10 @@ describe("saveClientTranslations", () => {
it("falls back to re-serialization and flags comment loss for unpatched keys", async () => {
state.readFile.mockResolvedValue('{"known": "old",}');
state.patchJson5.mockReturnValue({ content: '{"known": "old",}', unpatchedKeys: ["brand-new"] });
state.patchJson5.mockReturnValue({
content: '{"known": "old",}',
unpatchedKeys: ["brand-new"],
});
const res = await saveClientTranslations({
fileId: "ui-texts-it",
data: { known: "new", "brand-new": "val" },
@@ -260,18 +291,27 @@ describe("saveClientTranslations", () => {
fileId: "external-texts",
data: { x: "1" },
});
expect(res).toEqual({ ok: true, data: { commentsLost: true, unpatchedKeys: ["x"] } });
expect(res).toEqual({
ok: true,
data: { commentsLost: true, unpatchedKeys: ["x"] },
});
expect(state.patchJson5).toHaveBeenCalledWith("[1,2,3]", {}, { x: "1" });
});
it("validates fileId against the whitelist", async () => {
const res = await saveClientTranslations({ fileId: "hack" as never, data: {} });
const res = await saveClientTranslations({
fileId: "hack" as never,
data: {},
});
expect(res).toMatchObject({ ok: false, error: "Validation failed" });
});
it("requires the settings edit permission", async () => {
state.allowed = [];
const res = await saveClientTranslations({ fileId: "ui-texts-it", data: {} });
const res = await saveClientTranslations({
fileId: "ui-texts-it",
data: {},
});
expect(res).toEqual({ ok: false, error: "Unauthorized" });
});
+7 -7
View File
@@ -86,7 +86,9 @@ import { DatabaseError } from "@/lib/foundation/errors";
import { updateMotto, updateMottoAction } from "./user-settings";
const mockingForm = (motto: string): FormData =>
({ get: (key: string) => (key === "motto" ? motto : null) }) as unknown as FormData;
({
get: (key: string) => (key === "motto" ? motto : null),
}) as unknown as FormData;
beforeEach(() => {
vi.clearAllMocks();
@@ -99,9 +101,7 @@ beforeEach(() => {
describe("updateMotto", () => {
it("persists the motto, syncs RCON and revalidates /settings", async () => {
await updateMotto(
mockingForm(" hello world "),
);
await updateMotto(mockingForm(" hello world "));
expect(state.updateCall.values).toEqual({ motto: " hello world " });
expect(state.rconSetMotto).toHaveBeenCalledWith(42, " hello world ");
expect(mockRevalidatePath).toHaveBeenCalledWith("/settings");
@@ -126,9 +126,9 @@ describe("updateMotto", () => {
await expect(updateMotto(mockingForm("boom"))).rejects.toThrow(
databaseErrorClass,
);
await expect(
updateMotto(mockingForm("boom")),
).rejects.toThrow("Failed to update motto");
await expect(updateMotto(mockingForm("boom"))).rejects.toThrow(
"Failed to update motto",
);
});
it("does not sync RCON when the DB update fails", async () => {
+33 -14
View File
@@ -3,7 +3,12 @@ import { z } from "zod";
const state = vi.hoisted(() => ({
allowed: [] as string[],
watches: [] as { id: number; staffId: number; targetUserId: number; reason: string }[],
watches: [] as {
id: number;
staffId: number;
targetUserId: number;
reason: string;
}[],
insertCall: undefined as unknown,
deleteCall: undefined as unknown,
}));
@@ -19,7 +24,10 @@ const actionErrorClass = vi.hoisted(
);
vi.mock("@/lib/safe-action", () => ({
adminAction: (opts: { permission?: string | readonly string[]; schema?: z.ZodType }, handler: (ctx: any) => unknown) => {
adminAction: (
opts: { permission?: string | readonly string[]; schema?: z.ZodType },
handler: (ctx: any) => unknown,
) => {
return async (input: unknown) => {
const needed = Array.isArray(opts.permission)
? opts.permission
@@ -73,9 +81,11 @@ vi.mock("@/lib/db", async () => {
return [];
});
db.insert = vi.fn((table) => ({
values: (
values: { staffId: number; targetUserId: number; reason: string },
) => {
values: (values: {
staffId: number;
targetUserId: number;
reason: string;
}) => {
state.insertCall = { table, values };
return Promise.resolve([{ insertId: 1n }]);
},
@@ -101,14 +111,19 @@ beforeEach(() => {
describe("toggleUserWatch", () => {
it("creates a watch when none exists and returns watching: true", async () => {
const res = await toggleUserWatch({ targetUserId: 42, reason: "suspicious" });
const res = await toggleUserWatch({
targetUserId: 42,
reason: "suspicious",
});
expect(res).toEqual({ ok: true, data: { watching: true } });
expect(state.insertCall.values).toEqual({
staffId: 100,
targetUserId: 42,
reason: "suspicious",
});
expect(mockRevalidateTag).toHaveBeenCalledWith("user-watch:100", { expire: 0 });
expect(mockRevalidateTag).toHaveBeenCalledWith("user-watch:100", {
expire: 0,
});
});
it("defaults the reason to an empty string", async () => {
@@ -125,7 +140,9 @@ describe("toggleUserWatch", () => {
const res = await toggleUserWatch({ targetUserId: 42 });
expect(res).toEqual({ ok: true, data: { watching: false } });
expect(state.deleteCall.condition).toBeDefined();
expect(mockRevalidateTag).toHaveBeenCalledWith("user-watch:100", { expire: 0 });
expect(mockRevalidateTag).toHaveBeenCalledWith("user-watch:100", {
expire: 0,
});
});
it("requires the users view permission", async () => {
@@ -137,12 +154,14 @@ describe("toggleUserWatch", () => {
});
it("rejects invalid target user ids and oversized reasons", async () => {
expect(
await toggleUserWatch({ targetUserId: -1 }),
).toMatchObject({ ok: false, error: "Validation failed" });
expect(
await toggleUserWatch({ targetUserId: 0 }),
).toMatchObject({ ok: false, error: "Validation failed" });
expect(await toggleUserWatch({ targetUserId: -1 })).toMatchObject({
ok: false,
error: "Validation failed",
});
expect(await toggleUserWatch({ targetUserId: 0 })).toMatchObject({
ok: false,
error: "Validation failed",
});
expect(
await toggleUserWatch({ targetUserId: 1, reason: "x".repeat(256) }),
).toMatchObject({ ok: false, error: "Validation failed" });
+5 -1
View File
@@ -14,7 +14,11 @@ describe("CatalogPackages table schema", () => {
});
it("defines a uuid primary key", () => {
expect(columns.id).toMatchObject({ name: "id", primary: true, notNull: true });
expect(columns.id).toMatchObject({
name: "id",
primary: true,
notNull: true,
});
});
it("defines the package metadata columns as NOT NULL", () => {
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import type { MySqlIndex } from "drizzle-orm/mysql-core";
import { describe, expect, it } from "vitest";
import {
GamedataDocs,
GamedataFurnidata,
+2 -1
View File
@@ -10,7 +10,8 @@ vi.mock("node:worker_threads", () => {
class FakeWorker {
private handlers: Record<string, Handler[]> = {};
on(event: string, handler: Handler) {
(this.handlers[event] ??= []).push(handler);
if (!this.handlers[event]) this.handlers[event] = [];
this.handlers[event].push(handler);
return this;
}
emit(event: string, ...args: unknown[]) {
+7 -8
View File
@@ -24,6 +24,7 @@ vi.mock("@/lib/db", async () => {
query.where = self;
query.orderBy = self;
query.limit = self;
// biome-ignore lint/suspicious/noThenProperty: intentional thenable for await support
query.then = (
resolve: (value: unknown) => unknown,
reject: (error: unknown) => unknown,
@@ -45,11 +46,11 @@ import {
} from "@/lib/theme-blocks";
import { THEME_COLOR_KEYS } from "@/lib/theme-presets";
import {
type ResolvedTheme,
generateScopedCss,
getAllScopes,
getFullScopeValues,
getScopeValues,
type ResolvedTheme,
resolveTheme,
} from "./theme-resolver";
@@ -155,9 +156,7 @@ describe("resolveTheme", () => {
});
it("matches module scopes", async () => {
state.scopes = [
scope({ id: 1, type: "module", moduleId: "catalog" }),
];
state.scopes = [scope({ id: 1, type: "module", moduleId: "catalog" })];
state.values = [value(1, "color_primary", "#module")];
const resolved = await resolveTheme({ moduleId: "catalog" });
expect(resolved.palette.color_primary).toBe("#module");
@@ -212,7 +211,9 @@ describe("generateScopedCss", () => {
},
}),
);
expect(rootCss).toContain('--theme-logo-url:url("http://logo/\\"quoted\\"")');
expect(rootCss).toContain(
'--theme-logo-url:url("http://logo/\\"quoted\\"")',
);
expect(scopedCssBlocks).toHaveLength(3);
expect(scopedCssBlocks[0]).toMatch(/^\[data-theme-site="hotel\.test"\]\{/);
expect(scopedCssBlocks[0]).toContain("--color-primary:#111111;");
@@ -224,9 +225,7 @@ describe("generateScopedCss", () => {
describe("admin helpers", () => {
it("getAllScopes normalises bigint ids", async () => {
state.scopes = [
scope({ id: 7n, parentId: 3n, type: "site" }),
];
state.scopes = [scope({ id: 7n, parentId: 3n, type: "site" })];
const rows = await getAllScopes();
expect(rows[0]?.id).toBe(7);
expect(rows[0]?.parentId).toBe(3);
+10 -4
View File
@@ -68,8 +68,11 @@ export function createFakeActionDb(config: FakeActionDbConfig) {
config.ops.push(op);
const execute = () => run("insert", op);
return {
then: (onFulfilled: (value: unknown) => unknown, onRejected: (error: unknown) => unknown) =>
execute().then(onFulfilled, onRejected),
// biome-ignore lint/suspicious/noThenProperty: intentional thenable for await support
then: (
onFulfilled: (value: unknown) => unknown,
onRejected: (error: unknown) => unknown,
) => execute().then(onFulfilled, onRejected),
catch: (onRejected: (error: unknown) => unknown) =>
execute().catch(onRejected),
onDuplicateKeyUpdate: (dup: { set: Record<string, unknown> }) => {
@@ -115,8 +118,11 @@ export function createFakeActionDb(config: FakeActionDbConfig) {
query[method] = self;
}
const selectOp = (): FakeDbOp => ({ kind: "select", table, projection });
query.then = (onFulfilled: (value: unknown) => unknown, onRejected: (error: unknown) => unknown) =>
run("select", selectOp()).then(onFulfilled, onRejected);
// biome-ignore lint/suspicious/noThenProperty: intentional thenable for await support
query.then = (
onFulfilled: (value: unknown) => unknown,
onRejected: (error: unknown) => unknown,
) => run("select", selectOp()).then(onFulfilled, onRejected);
query.catch = (onRejected: (error: unknown) => unknown) =>
run("select", selectOp()).catch(onRejected);
return query;
+3 -1
View File
@@ -44,10 +44,12 @@ function makeQuery(
]) {
query[method] = self;
}
// biome-ignore lint/suspicious/noThenProperty: intentional thenable for await support
query.then = (
onFulfilled: (value: unknown) => unknown,
onRejected: (error: unknown) => unknown,
) => Promise.resolve(resolve(table, projection)).then(onFulfilled, onRejected);
) =>
Promise.resolve(resolve(table, projection)).then(onFulfilled, onRejected);
query.catch = (onRejected: (error: unknown) => unknown) =>
Promise.resolve(resolve(table, projection)).catch(onRejected);
return query;