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
41 files changed
+488 -314

No files matched your search

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