lint: fix biome issues in new test files and helpers
This commit is contained in:
1 parent
99eb3af17b
commit
93862c2275
38 files changed
+423
-249
No files matched your search
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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" }),
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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", () => ({
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
@@ -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" });
|
||||
});
|
||||
});
|
||||
@@ -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" });
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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" });
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user