/** * Integration tests for POST /api/po/import. * Tests authorization guards and end-to-end parsing of the Sample_PO.xlsx * fixture using the real route handler. */ import { vi, describe, it, expect, beforeAll } from "vitest"; vi.mock("@/auth", () => ({ auth: vi.fn() })); import { auth } from "@/auth"; import { readFileSync } from "fs"; import { resolve } from "path"; import { NextRequest } from "next/server"; import { POST } from "@/app/api/po/import/route"; import { makeSession, getSeedUser } from "./helpers"; import type { ParsedImport } from "@/lib/po-import-parser"; const SAMPLE_XLSX = resolve(__dirname, "../../../../Prototype/Sample_PO.xlsx"); let techId: string; let managerId: string; let accountsId: string; beforeAll(async () => { const [tech, mgr, acct] = await Promise.all([ getSeedUser("tech@pelagia.local"), getSeedUser("manager@pelagia.local"), getSeedUser("accounts@pelagia.local"), ]); techId = tech.id; managerId = mgr.id; accountsId = acct.id; }); function makeFileRequest(filePath?: string) { const formData = new FormData(); if (filePath) { const buffer = readFileSync(filePath); const file = new File( [buffer], "import.xlsx", { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" } ); formData.append("file", file); } return new NextRequest("http://localhost/api/po/import", { method: "POST", body: formData }); } // ── Authorization ───────────────────────────────────────────────────────────── describe("POST /api/po/import — authorization", () => { it("returns 401 for unauthenticated requests", async () => { vi.mocked(auth).mockResolvedValue(null); const res = await POST(makeFileRequest(SAMPLE_XLSX)); expect(res.status).toBe(401); }); it("returns 403 for TECHNICAL role", async () => { vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const res = await POST(makeFileRequest(SAMPLE_XLSX)); expect(res.status).toBe(403); const data = await res.json(); expect(data.error).toMatch(/forbidden/i); }); it("returns 403 for ACCOUNTS role", async () => { vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS")); const res = await POST(makeFileRequest(SAMPLE_XLSX)); expect(res.status).toBe(403); }); it("returns 200 for MANAGER role with valid file", async () => { vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); const res = await POST(makeFileRequest(SAMPLE_XLSX)); expect(res.status).toBe(200); }); }); // ── Input validation ────────────────────────────────────────────────────────── describe("POST /api/po/import — input validation", () => { beforeEach(() => { vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); }); it("returns 400 when no file is provided", async () => { const res = await POST(makeFileRequest()); expect(res.status).toBe(400); const data = await res.json(); expect(data.error).toBeDefined(); }); it("returns 400 for a non-XLSX binary file", async () => { const formData = new FormData(); const garbage = new File([Buffer.from("not-an-xlsx")], "bad.xlsx", { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }); formData.append("file", garbage); const req = new NextRequest("http://localhost/api/po/import", { method: "POST", body: formData }); const res = await POST(req); expect(res.status).toBe(400); }); }); // ── Parsing results ─────────────────────────────────────────────────────────── describe("POST /api/po/import — parsing Sample_PO.xlsx", () => { let results: ParsedImport[]; beforeAll(async () => { vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); const res = await POST(makeFileRequest(SAMPLE_XLSX)); const data = await res.json(); results = data.results; }); it("returns exactly one result (one sheet with PO data)", () => { expect(results).toHaveLength(1); }); it("extracted line items contain no T&C rows", () => { const items = results[0].lineItems; const hasTcText = items.some( (li) => li.description.toLowerCase().includes("please quote") || li.description.toLowerCase().includes("delivery :") || li.description.toLowerCase().includes("payment terms") ); expect(hasTcText).toBe(false); }); it("extracted exactly one line item", () => { expect(results[0].lineItems).toHaveLength(1); }); it("line item has correct description", () => { expect(results[0].lineItems[0].description).toBe("Eni EP 80W90 GEAR OIL"); }); it("line item has correct quantity (1050)", () => { expect(results[0].lineItems[0].quantity).toBe(1050); }); it("line item has correct unit price (182)", () => { expect(results[0].lineItems[0].unitPrice).toBe(182); }); it("line item has GST rate 0.18", () => { expect(results[0].lineItems[0].gstRate).toBeCloseTo(0.18); }); it("vendor name extracted correctly", () => { expect(results[0].vendorName).toBe("Apar Industries Ltd"); }); it("PI quotation number extracted", () => { expect(results[0].piQuotationNo).toBe("Verbal"); }); it("delivery T&C stripped of prefix", () => { expect(results[0].tcDelivery).not.toMatch(/^DELIVERY\s*:/i); expect(results[0].tcDelivery).toMatch(/4 to 5 days/i); }); });