test(po-pdf): cover token capture + proxy routing
This commit is contained in:
parent
6a7dcc0af2
commit
f146e42ed0
1 changed files with 243 additions and 0 deletions
243
src/app/api/v1/purchase-orders/[id]/pdf/route.test.ts
Normal file
243
src/app/api/v1/purchase-orders/[id]/pdf/route.test.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
// Mocks
|
||||
const getOneMock = vi.fn();
|
||||
const updateMock = vi.fn();
|
||||
const fetchMock = vi.fn();
|
||||
|
||||
vi.mock("@/lib/server/pocketbase-admin", () => ({
|
||||
getAdminPocketBase: vi.fn().mockResolvedValue({
|
||||
collection: (name: string) => {
|
||||
if (name === "order_items") {
|
||||
return {
|
||||
getFullList: vi
|
||||
.fn()
|
||||
.mockResolvedValue([
|
||||
{ id: "item-1", quantity: 1, cost_price: 100, purchase_order: "po-1" },
|
||||
]),
|
||||
};
|
||||
}
|
||||
return {
|
||||
getOne: getOneMock,
|
||||
update: updateMock,
|
||||
};
|
||||
},
|
||||
files: { getURL: vi.fn() },
|
||||
authStore: { token: "test-token" },
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/letterhead", () => ({
|
||||
fetchLetterhead: vi.fn().mockResolvedValue({ id: "cnl", name: "CNL" }),
|
||||
loadLetterheadLogoDataUri: vi.fn().mockResolvedValue("data:image/svg+xml;base64,AAA"),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/pdf/render", () => ({
|
||||
renderPurchaseOrderPdf: vi.fn().mockResolvedValue(Buffer.from("PDF")),
|
||||
}));
|
||||
|
||||
import { GET, POST } from "./route";
|
||||
|
||||
function makeReq(url: string, init: { method?: string; body?: unknown } = {}) {
|
||||
return new NextRequest(url, {
|
||||
method: init.method ?? "GET",
|
||||
headers: { cookie: "test-cookie" },
|
||||
body: init.body !== undefined ? JSON.stringify(init.body) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
describe("/api/v1/purchase-orders/[id]/pdf", () => {
|
||||
beforeEach(() => {
|
||||
getOneMock.mockReset();
|
||||
updateMock.mockReset();
|
||||
fetchMock.mockReset();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("POST — token capture", () => {
|
||||
it("stores the PB token, not the human-readable filename, in documents_history", async () => {
|
||||
updateMock
|
||||
.mockResolvedValueOnce({
|
||||
documents: ["slug-token-v3.pdf", "slug-token-v4.pdf"],
|
||||
})
|
||||
.mockResolvedValueOnce({});
|
||||
|
||||
getOneMock.mockResolvedValueOnce({
|
||||
id: "po-1",
|
||||
po_number: "PO-2026-07-009",
|
||||
order: "order-1",
|
||||
supplier: "supplier-1",
|
||||
expand: { order: { id: "order-1" }, supplier: { id: "supplier-1" } },
|
||||
documents: ["slug-token-v3.pdf"],
|
||||
documents_history: [
|
||||
{
|
||||
version: 3,
|
||||
file: "PO-CNL-PO-2026-07-009-v3.pdf",
|
||||
token: "slug-token-v3.pdf",
|
||||
generatedAt: "2026-07-09T00:00:00Z",
|
||||
generatedBy: "",
|
||||
reason: "",
|
||||
status: "current",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const req = makeReq("http://localhost/api/v1/purchase-orders/po-1/pdf", {
|
||||
method: "POST",
|
||||
body: {},
|
||||
});
|
||||
|
||||
const res = await POST(req, { params: Promise.resolve({ id: "po-1" }) });
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const historyCall = updateMock.mock.calls[1];
|
||||
expect(historyCall[1]).toEqual({
|
||||
documents_history: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
version: 4,
|
||||
file: "PO-CNL-PO-2026-07-009-v4.pdf",
|
||||
token: "slug-token-v4.pdf",
|
||||
status: "current",
|
||||
}),
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET — proxy routing", () => {
|
||||
it("streams the PDF through the file proxy using entry.token", async () => {
|
||||
getOneMock.mockResolvedValueOnce({
|
||||
id: "po-1",
|
||||
po_number: "PO-2026-07-009",
|
||||
documents: ["slug-v4.pdf"],
|
||||
documents_history: [
|
||||
{
|
||||
version: 4,
|
||||
file: "PO-CNL-PO-2026-07-009-v4.pdf",
|
||||
token: "slug-v4.pdf",
|
||||
generatedAt: "2026-07-09T00:00:00Z",
|
||||
generatedBy: "",
|
||||
reason: "",
|
||||
status: "current",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
new Response(Buffer.from("PDF bytes"), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/pdf" },
|
||||
}),
|
||||
);
|
||||
|
||||
const req = makeReq("http://localhost/api/v1/purchase-orders/po-1/pdf?version=4");
|
||||
const res = await GET(req, { params: Promise.resolve({ id: "po-1" }) });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const [url] = fetchMock.mock.calls[0];
|
||||
expect(String(url)).toContain("/api/v1/files/purchase_orders/po-1/slug-v4.pdf");
|
||||
});
|
||||
|
||||
it("falls back to documents[i] when entry.token is missing", async () => {
|
||||
getOneMock.mockResolvedValueOnce({
|
||||
id: "po-1",
|
||||
po_number: "PO-2026-07-009",
|
||||
documents: ["legacy-token.pdf"],
|
||||
documents_history: [
|
||||
{
|
||||
version: 2,
|
||||
file: "PO-CNL-PO-2026-07-009-v2.pdf",
|
||||
generatedAt: "2026-07-09T00:00:00Z",
|
||||
generatedBy: "",
|
||||
reason: "",
|
||||
status: "superseded",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
new Response(Buffer.from("PDF bytes"), { status: 200 }),
|
||||
);
|
||||
|
||||
const req = makeReq("http://localhost/api/v1/purchase-orders/po-1/pdf?version=2");
|
||||
const res = await GET(req, { params: Promise.resolve({ id: "po-1" }) });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const [url] = fetchMock.mock.calls[0];
|
||||
expect(String(url)).toContain("/api/v1/files/purchase_orders/po-1/legacy-token.pdf");
|
||||
});
|
||||
|
||||
it("404s with a clear message when no token is resolvable", async () => {
|
||||
getOneMock.mockResolvedValueOnce({
|
||||
id: "po-1",
|
||||
po_number: "PO-2026-07-009",
|
||||
documents: [],
|
||||
documents_history: [
|
||||
{
|
||||
version: 1,
|
||||
file: "PO-CNL-PO-2026-07-009-v1.pdf",
|
||||
generatedAt: "2026-07-09T00:00:00Z",
|
||||
generatedBy: "",
|
||||
reason: "",
|
||||
status: "current",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const req = makeReq("http://localhost/api/v1/purchase-orders/po-1/pdf");
|
||||
const res = await GET(req, { params: Promise.resolve({ id: "po-1" }) });
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
const body = await res.json();
|
||||
expect(body.error).toMatch(/token missing/i);
|
||||
});
|
||||
|
||||
it("respects ?version=N and ?download=1", async () => {
|
||||
getOneMock.mockResolvedValueOnce({
|
||||
id: "po-1",
|
||||
po_number: "PO-2026-07-009",
|
||||
documents: ["slug-v2.pdf"],
|
||||
documents_history: [
|
||||
{
|
||||
version: 1,
|
||||
file: "PO-CNL-PO-2026-07-009-v1.pdf",
|
||||
token: "slug-v1.pdf",
|
||||
generatedAt: "2026-07-09T00:00:00Z",
|
||||
generatedBy: "",
|
||||
reason: "",
|
||||
status: "superseded",
|
||||
},
|
||||
{
|
||||
version: 2,
|
||||
file: "PO-CNL-PO-2026-07-009-v2.pdf",
|
||||
token: "slug-v2.pdf",
|
||||
generatedAt: "2026-07-09T00:00:00Z",
|
||||
generatedBy: "",
|
||||
reason: "",
|
||||
status: "current",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
new Response(Buffer.from("PDF"), { status: 200 }),
|
||||
);
|
||||
|
||||
const req = makeReq(
|
||||
"http://localhost/api/v1/purchase-orders/po-1/pdf?version=1&download=1",
|
||||
);
|
||||
const res = await GET(req, { params: Promise.resolve({ id: "po-1" }) });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const [url] = fetchMock.mock.calls[0];
|
||||
expect(String(url)).toContain("/api/v1/files/purchase_orders/po-1/slug-v1.pdf");
|
||||
expect(res.headers.get("Content-Disposition")).toMatch(/^attachment/);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue