lint: fix biome issues in new test files and helpers
This commit is contained in:
1 parent
99eb3af17b
commit
93862c2275
41 files changed
+488
-314
No files matched your search
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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)",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -158,4 +158,4 @@ describe("deleteWord", () => {
|
|||||||
error: "Could not remove word",
|
error: "Could not remove word",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -156,4 +156,4 @@ describe("signOutAndRevokeTicket", () => {
|
|||||||
);
|
);
|
||||||
expect(state.signOut).toHaveBeenCalledWith({ redirectTo: "/" });
|
expect(state.signOut).toHaveBeenCalledWith({ redirectTo: "/" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+32
-21
@@ -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
@@ -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",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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
@@ -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" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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
@@ -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" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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[]) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user