WFDASH-WEBAPP/src/lib/server/collection-confirm-upload.test.ts
SJCoetz 12fa65a481 feat: add new order, invoice, and dashboard management tools
- Add server-side `/api/v1/dashboard` endpoint to reduce client-side payload and round trips
- Implement invoice upload and order-item SKU update APIs with complete test suites
- Refactor order and order-items list endpoints (including MCP tools) to support proper PocketBase filter and sort params
- Fix pagination validation for order-items route to clamp page ≥1 and perPage 1-500
- Add redirects in next.config.ts to map underscore collection paths to hyphenated API routes
- Improve buyer tasks page to avoid redundant tab count refetches when switching tabs
- Refactor collection confirm components with editable SKUs and updated accessibility labels
- Add cached admin PocketBase auth to minimize repeated authentication calls
- Add performance testing scripts for dashboard endpoint latency
- Update quotes list route to default to excluding archived counts
- Fix photo strip component text and labels for non-invoice documents
- Refactor sprint page to lazy load supplier/customer data for pricing modal
- Clean up unused imports in test files
- Add full test coverage for all new APIs, services, and components
2026-08-05 23:00:54 +02:00

101 lines
3.8 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from "vitest";
vi.mock("@/lib/server/pocketbase-admin", () => ({
getAdminPocketBase: vi.fn(),
}));
vi.mock("@/lib/server/orders-service", () => ({
getOrderItemsCollectionName: vi.fn(() => "order_items_v2"),
}));
import { getAdminPocketBase } from "@/lib/server/pocketbase-admin";
import { uploadInvoiceToOrderItems } from "./collection-confirm-service";
/**
* Mock admin client factory for the upload path: a single
* `getFullList` (validate the items are still `collected`) and
* a `collection(...).update(id, formData)` per matched item.
*/
const makeAdmin = (opts: {
matches?: Array<{ id: string; item_status: string }>;
rejects?: boolean;
}) => {
const update = vi.fn().mockImplementation(async () => {
if (opts.rejects) throw new Error("PB write failed");
return { id: "ignored" };
});
const getFullList = vi.fn().mockResolvedValue(opts.matches ?? []);
const admin = {
collection: vi.fn().mockImplementation(() => ({ update, getFullList })),
};
return { admin, update, getFullList };
};
describe("uploadInvoiceToOrderItems", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("rejects an empty id list with a 400", async () => {
const { admin, update } = makeAdmin({});
vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never);
const file = new File(["pdf"], "inv.pdf", { type: "application/pdf" });
await expect(uploadInvoiceToOrderItems([], file)).rejects.toMatchObject({
code: "invalid_item_ids",
status: 400,
});
expect(update).not.toHaveBeenCalled();
});
it("rejects a missing file", async () => {
const { admin } = makeAdmin({});
vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never);
await expect(
uploadInvoiceToOrderItems(["row-1"], null as unknown as File),
).rejects.toMatchObject({ code: "missing_file", status: 400 });
});
it("rejects an oversized file", async () => {
const { admin } = makeAdmin({});
vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never);
const oversize = new File([new Uint8Array(26 * 1024 * 1024)], "big.pdf", {
type: "application/pdf",
});
await expect(
uploadInvoiceToOrderItems(["row-1"], oversize),
).rejects.toMatchObject({ code: "file_too_large", status: 413 });
});
it("rejects when none of the supplied ids are still collected", async () => {
const { admin, update } = makeAdmin({ matches: [] });
vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never);
const file = new File(["pdf"], "inv.pdf", { type: "application/pdf" });
await expect(uploadInvoiceToOrderItems(["row-1"], file)).rejects.toMatchObject({
code: "items_not_collected",
status: 409,
});
expect(update).not.toHaveBeenCalled();
});
it("uploads the file to every still-collected row and prefixes the filename", async () => {
const { admin, update } = makeAdmin({
matches: [
{ id: "row-1", item_status: "collected" },
{ id: "row-2", item_status: "collected" },
{ id: "row-3", item_status: "ready_invoice" }, // should be skipped
],
});
vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never);
const file = new File(["pdf"], "INV-1.pdf", { type: "application/pdf" });
const result = await uploadInvoiceToOrderItems(["row-1", "row-2", "row-3"], file);
expect(result.filename).toBe("[invoice] INV-1.pdf");
expect(result.itemIds).toEqual(["row-1", "row-2"]);
expect(update).toHaveBeenCalledTimes(2);
const form = update.mock.calls[0]?.[1] as FormData;
expect(form).toBeInstanceOf(FormData);
const uploaded = form.get("order_item_attachements") as File;
expect(uploaded).toBeInstanceOf(File);
expect(uploaded.name).toBe("[invoice] INV-1.pdf");
});
});