- 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
101 lines
3.8 KiB
TypeScript
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");
|
|
});
|
|
});
|