Files
Epicnabbo-Catalogus-Updated…/src/features/catalog/server/bulk-actions.test.ts
T

115 lines
4.3 KiB
TypeScript

import { beforeEach, expect, it, vi } from "vitest";
const state = vi.hoisted(() => ({
permission: vi.fn(),
preview: vi.fn(),
destinations: vi.fn(),
apply: vi.fn(),
undo: vi.fn(),
export: vi.fn(),
send: vi.fn(),
audit: vi.fn(),
refresh: vi.fn(),
}));
vi.mock("@/lib/admin/guard", () => ({ requirePermission: state.permission }));
vi.mock("@/lib/permissions", () => ({
PERMS: { CATALOG_VIEW: "view", CATALOG_EDIT: "edit" },
}));
vi.mock("@/features/catalog/server/bulk-offers", () => ({
previewBulkOffersCommand: state.preview,
listBulkOfferDestinationsCommand: state.destinations,
applyBulkOffersCommand: state.apply,
undoBulkOffersCommand: state.undo,
}));
vi.mock("@/lib/services/catalog-git-queue", () => ({
withCatalogExport: state.export,
}));
vi.mock("@/features/catalog/server/sync-status", () => ({
sendCatalogUpdate: state.send,
}));
vi.mock("@/lib/services/audit", () => ({ logAudit: state.audit }));
vi.mock("@/lib/logger", () => ({ logger: { error: vi.fn() } }));
vi.mock("next/cache", () => ({ revalidatePath: state.refresh }));
import {
applyBulkOffers,
getBulkOfferDestinations,
previewBulkOffers,
undoBulkOffers,
} from "@/actions/catalog-bulk";
const input = { ids: [1], changes: { pageId: 2 } };
beforeEach(() => {
vi.clearAllMocks();
state.permission.mockResolvedValue({ id: 1 });
state.export.mockImplementation((fn) => fn());
state.apply.mockResolvedValue({ changedCount: 1 });
state.preview.mockResolvedValue({ fingerprint: "x" });
state.audit.mockResolvedValue(undefined);
});
it("gates read preview without scheduling any mutation", async () => {
await previewBulkOffers(input);
expect(state.permission).toHaveBeenCalledWith("view");
expect(state.export).not.toHaveBeenCalled();
expect(state.apply).not.toHaveBeenCalled();
});
it("gates edits before database and sends one update after commit", async () => {
expect((await applyBulkOffers(input, "x")).ok).toBe(true);
expect(state.permission).toHaveBeenCalledWith("edit");
expect(state.export).toHaveBeenCalledTimes(1);
expect(state.send).not.toHaveBeenCalled();
expect(state.refresh).toHaveBeenCalled();
});
it("stops denied permission before export or data access", async () => {
state.permission.mockRejectedValueOnce(Error("Denied"));
await expect(applyBulkOffers(input, "x")).rejects.toThrow("Denied");
expect(state.export).not.toHaveBeenCalled();
});
it("does not notify hotel when a transaction fails", async () => {
state.apply.mockRejectedValueOnce(Error("failed"));
expect((await applyBulkOffers(input, "x")).ok).toBe(false);
expect(state.send).not.toHaveBeenCalled();
expect(state.audit).not.toHaveBeenCalled();
});
it("does not report a committed edit as failed on audit failure", async () => {
state.audit.mockRejectedValueOnce(Error("log failed"));
expect((await applyBulkOffers(input, "x")).ok).toBe(true);
expect(state.send).not.toHaveBeenCalled();
});
it("loads destination choices with view permission only", async () => {
state.destinations.mockResolvedValueOnce({
pages: [{ id: 9, caption: "Target" }],
});
expect(await getBulkOfferDestinations()).toEqual({
ok: true,
data: { pages: [{ id: 9, caption: "Target" }] },
});
expect(state.permission).toHaveBeenCalledWith("view");
expect(state.export).not.toHaveBeenCalled();
});
it("undo enforces edit access and exports only successful restores", async () => {
state.undo.mockResolvedValueOnce({ changedCount: 2 });
expect((await undoBulkOffers([10, 11])).ok).toBe(true);
expect(state.permission).toHaveBeenCalledWith("edit");
expect(state.undo).toHaveBeenCalledWith([10, 11], 1, undefined);
expect(state.export).toHaveBeenCalledTimes(1);
expect(state.send).not.toHaveBeenCalled();
});
it("undo denial stops before export and a conflict does not notify", async () => {
state.permission.mockRejectedValueOnce(Error("Denied"));
await expect(undoBulkOffers([10])).rejects.toThrow("Denied");
expect(state.export).not.toHaveBeenCalled();
state.undo.mockRejectedValueOnce(Error("conflict"));
expect((await undoBulkOffers([10])).ok).toBe(false);
expect(state.send).not.toHaveBeenCalled();
});
it("passes a stable operation key and does not synchronously notify after commit", async () => {
const key = "123e4567-e89b-42d3-a456-426614174000";
expect((await applyBulkOffers(input, "x", key)).ok).toBe(true);
expect(state.apply).toHaveBeenCalledWith(input, "x", 1, key);
expect(state.send).not.toHaveBeenCalled();
});