pelagia-portal/App/tests/integration/import-api.test.ts
2026-05-18 23:18:58 +05:30

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);
});
});