pelagia-portal/App/tests/integration/import-api.test.ts
Hardik 938ff6df89 test+ci: green the test baseline and make type-check + unit tests hard gates
Green-lights the test suite so the PR checks can enforce it:
- Fix the NextAuth v5 auth() mock typing across all integration tests (cast to a
  simple async fn so mockResolvedValue accepts the session) — clears ~86 errors.
- Fix stale test values: intent 'resubmit'->'submit' / 'save'->'draft'; ParsedImportLine
  .description -> .name; approvepo -> approvePo; add missing beforeEach/beforeAll imports.
- permissions: MANAGER *can* process_payment (intentional since e1340b9) — update the
  stale assertion.
- po-import-parser: skip the Sample_PO.xlsx fixture tests when the file is absent (it
  lives outside the repo); synthetic-workbook tests still cover the parser.

type-check is now 0 errors and unit tests pass (167 passed, 13 skipped). pr-checks.yml
flips type-check (whole project) and unit tests to HARD gates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 13:03:54 +05:30

162 lines
5.8 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, beforeEach } 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 as unknown as () => Promise<unknown>).mockResolvedValue(null);
const res = await POST(makeFileRequest(SAMPLE_XLSX));
expect(res.status).toBe(401);
});
it("returns 403 for TECHNICAL role", async () => {
vi.mocked(auth as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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.name.toLowerCase().includes("please quote") ||
li.name.toLowerCase().includes("delivery :") ||
li.name.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].name).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);
});
});