// Run the real PB hook in a JS sandbox; no live records are changed. import { readFileSync } from "node:fs"; import { runInNewContext } from "node:vm"; import { describe, expect, it, vi } from "vitest"; function setup(stored: Record = {}, collection = "order_items") { let hook!: (event: unknown) => void; let capability!: (event: unknown) => void; let createHook!: (event: unknown) => void; const next = vi.fn(); const body = { _collection_return_guard: { updated: "v1", parent_updated: "p1" } }; const parent = { id: "po1", updated: "p1", status: "received", booked_at: "" }; const app = { findRecordById: vi.fn((name: string) => name === "stock_purchases" ? parent : original), findRecordsByFilter: vi.fn((): unknown[] => []), runInTransaction: (fn: (tx: unknown) => void) => fn(app), }; const info = { body, context: "batch" }; const original = { id: "row1", updated: "v1", item_status: "collected", status: "collected", purchase: "po1", ...stored }; const e = { requestInfo: () => info, auth: { isSuperuser: () => true }, app, next, record: { original: () => original, collection: () => ({ name: collection }), getString: (field: string) => String((original as Record)[field] ?? "") }, }; const source = readFileSync("pocketbase/pb_hooks/collection_return.pb.js", "utf8"); runInNewContext(source, { onRecordUpdateRequest: (callback: typeof hook) => { hook = callback; }, onRecordCreateRequest: (callback: typeof hook) => { createHook = callback; }, routerAdd: (_method: string, _path: string, callback: typeof capability) => { capability = callback; }, $apis: { requireSuperuserAuth: () => ({}) }, BadRequestError: Error, ForbiddenError: Error, }); return { call: () => hook(e), create: () => createHook(e), e, next, app, info, parent, capability, original }; } describe("PB collection return transaction guard", () => { it("allows a matching collected revision inside a batch", () => { const test = setup(); test.call(); expect(test.next).toHaveBeenCalledOnce(); }); it("leaves ordinary updates alone", () => { const test = setup(); test.info.body = {} as typeof test.info.body; test.call(); expect(test.next).toHaveBeenCalledOnce(); }); it.each([ { updated: "v2" }, { item_status: "ready_invoice" }, { qty_delivered: 1 }, { order_item_details: { invoice: { number: "INV1", qbInvoiceId: "qb1" } } }, { order_item_details: JSON.stringify({ invoice: { number: "INV1", billingCompany: "CNL" } }) }, ])("refuses a concurrent advance: %j", stored => { const test = setup(stored); expect(test.call).toThrow(); expect(test.next).not.toHaveBeenCalled(); }); it("refuses non-batch resets", () => { const test = setup(); test.info.context = "default"; expect(test.call).toThrow(); }); it("refuses non-superusers", () => { const test = setup(); test.e.auth.isSuperuser = () => false; expect(test.call).toThrow(); }); it("accepts a supplier-invoice blob on a still-collected customer item", () => { const test = setup({ order_item_details: { invoice: { number: "SUP1" } } }); test.call(); expect(test.next).toHaveBeenCalledOnce(); }); it("refuses a stock parent changed after the app's check", () => { const test = setup({}, "stock_purchase_items"); test.parent.updated = "p2"; expect(test.call).toThrow(); }); it("refuses a stock booking that started after the app's check", () => { const test = setup({}, "stock_purchase_items"); test.app.findRecordsByFilter.mockReturnValue([{ id: "new-receipt" }]); expect(test.call).toThrow(); }); it("allows an unbooked received header to reopen", () => { const test = setup({ status: "received" }, "stock_purchases"); test.call(); expect(test.next).toHaveBeenCalledOnce(); }); it("refuses a stock header booked between checking and saving", () => { const test = setup({ status: "received", booked_at: "today" }, "stock_purchases"); expect(test.call).toThrow(); }); it("refuses stale confirmation after a reset has committed", () => { const test = setup({ item_status: "collection_pool" }); test.info.body = { item_status: "ready_invoice" } as unknown as typeof test.info.body; expect(test.call).toThrow(); expect(test.next).not.toHaveBeenCalled(); }); it("allows normal collection confirmation", () => { const test = setup(); test.info.body = { item_status: "ready_invoice" } as unknown as typeof test.info.body; test.call(); expect(test.next).toHaveBeenCalledOnce(); }); it("refuses a stale stock booking ledger insert after a reset reopens the PO", () => { const test = setup({ reference_type: "stock_purchase", reference_id: "po1" }); test.parent.status = "sent"; expect(test.create).toThrow(); expect(test.next).not.toHaveBeenCalled(); }); it("allows a stock booking ledger insert while the PO is received", () => { const test = setup({ reference_type: "stock_purchase", reference_id: "po1" }); test.create(); expect(test.next).toHaveBeenCalledOnce(); }); });