90 lines
3.2 KiB
TypeScript
90 lines
3.2 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(),
|
|
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();
|
|
});
|