- 100% coverage on 58 src/actions/*.ts, 24 src/lib/services/*.ts, 19 src/features|db|hooks|i18n/*.ts - 3 core lib modules (theme-resolver, ip-lookup, translation-pool): 100% - ~3,000 new meaningful tests - Bugfixes: - theme-resolver: generateScopedCss now emits scoped CSS blocks (was early-return bug) - admin-radio-api-keys: blank rateLimit now uses fallback - admin-badge-upload: validation before try-block to prevent swallowed redirect - Coverage raised from 26% -> 34% statements
112 lines
3.2 KiB
TypeScript
112 lines
3.2 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
staff: { id: 11, rank: 7, username: "staff" },
|
|
requirePermissionRateLimited: vi.fn(),
|
|
listingRows: [] as unknown[],
|
|
updateSet: vi.fn(),
|
|
updateWhere: vi.fn(async () => [{ affectedRows: 1 }]),
|
|
logStaffActivity: vi.fn(async () => undefined),
|
|
revalidatePath: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/lib/admin/guard", () => ({
|
|
requirePermissionRateLimited: mocks.requirePermissionRateLimited,
|
|
}));
|
|
vi.mock("@/lib/permissions", () => ({
|
|
PERMS: { SHOP_EDIT: "admin.shop.edit" },
|
|
}));
|
|
vi.mock("@/lib/services/staff-activity", () => ({
|
|
logStaffActivity: mocks.logStaffActivity,
|
|
}));
|
|
vi.mock("next/cache", () => ({ revalidatePath: mocks.revalidatePath }));
|
|
vi.mock("@/lib/db", async () => {
|
|
const schema = await import("@/db/schema");
|
|
const { createFakeDb } = await import("@/test/fake-db");
|
|
const fake = createFakeDb(() => mocks.listingRows);
|
|
return {
|
|
...schema,
|
|
db: {
|
|
...fake,
|
|
update: vi.fn(() => ({ set: mocks.updateSet })),
|
|
},
|
|
};
|
|
});
|
|
|
|
import { PERMS } from "@/lib/permissions";
|
|
import { cancelMarketplaceListing } from "./admin-marketplace";
|
|
|
|
function form(id: string) {
|
|
const f = new FormData();
|
|
f.set("id", id);
|
|
return f;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mocks.requirePermissionRateLimited.mockResolvedValue(mocks.staff);
|
|
mocks.listingRows = [];
|
|
mocks.updateSet.mockReturnValue({ where: mocks.updateWhere });
|
|
mocks.updateWhere.mockResolvedValue([{ affectedRows: 1 }]);
|
|
});
|
|
|
|
describe("cancelMarketplaceListing", () => {
|
|
it("cancels an active listing, logs activity and revalidates", async () => {
|
|
mocks.listingRows = [
|
|
{ id: 5, state: 1, userId: 3, itemId: 100, price: 250 },
|
|
];
|
|
|
|
await cancelMarketplaceListing(form("5"));
|
|
|
|
expect(mocks.requirePermissionRateLimited).toHaveBeenCalledWith(
|
|
PERMS.SHOP_EDIT,
|
|
);
|
|
expect(mocks.updateSet).toHaveBeenCalledWith({ state: 0 });
|
|
expect(mocks.logStaffActivity).toHaveBeenCalledWith({
|
|
staffId: 11,
|
|
action: "marketplace_cancel",
|
|
description:
|
|
"Cancelled marketplace listing #5 (item 100, user 3, price 250)",
|
|
targetType: "marketplace",
|
|
targetId: 5,
|
|
});
|
|
expect(mocks.revalidatePath).toHaveBeenCalledWith("/admin/marketplace");
|
|
});
|
|
|
|
it("returns early for a non-positive id", async () => {
|
|
await cancelMarketplaceListing(form("0"));
|
|
|
|
expect(mocks.updateSet).not.toHaveBeenCalled();
|
|
expect(mocks.logStaffActivity).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does nothing when the listing does not exist", async () => {
|
|
mocks.listingRows = [];
|
|
|
|
await cancelMarketplaceListing(form("8"));
|
|
|
|
expect(mocks.updateSet).not.toHaveBeenCalled();
|
|
expect(mocks.revalidatePath).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does nothing when the listing is not active", async () => {
|
|
mocks.listingRows = [{ id: 9, state: 0, userId: 1, itemId: 2, price: 3 }];
|
|
|
|
await cancelMarketplaceListing(form("9"));
|
|
|
|
expect(mocks.updateSet).not.toHaveBeenCalled();
|
|
expect(mocks.revalidatePath).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("propagates a permission denial", async () => {
|
|
mocks.requirePermissionRateLimited.mockRejectedValueOnce(
|
|
new Error("redirect:/admin"),
|
|
);
|
|
|
|
await expect(cancelMarketplaceListing(form("1"))).rejects.toThrow(
|
|
"redirect:/admin",
|
|
);
|
|
expect(mocks.updateSet).not.toHaveBeenCalled();
|
|
});
|
|
});
|