import { beforeEach, expect, it, vi } from "vitest"; const state = vi.hoisted(() => ({ permission: vi.fn(), preview: vi.fn(), destinations: vi.fn(), apply: 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, })); 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, } 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).toHaveBeenCalledTimes(1); expect(state.apply.mock.invocationCallOrder[0]).toBeLessThan( state.send.mock.invocationCallOrder[0], ); }); 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).toHaveBeenCalledTimes(1); }); 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(); });