162 lines
5.6 KiB
TypeScript
162 lines
5.6 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|