test(integration): repair stale integration suite (wip: 97/108)

The integration suite had rotted against the app. Systematic fixes:
- seed refs: MV Ocean Pride/Sea Breeze/TECH-OPS → current seed entities
- helper appendLineItem set lineItems[i].description; createPo now keys on
  lineItems[i].name → zero line items. Fixed to .name.
- vendor gating: lifecycle setups (approval/payment/receipt) now attach the
  seeded verified vendor before approval.
- cleanup: POAction has no onDelete:Cascade, so deletePo(sByTitle) now removes
  POAction rows before the PO.
- import-api: fixture committed to tests/fixtures/Sample_PO.xlsx (was an
  absolute path to a non-existent dir).
- products-search: code search assertion .every → .some (search spans fields).

11 failures remain (behavioral drift — separate commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hardik 2026-06-21 02:34:07 +05:30
parent 74d20cd452
commit b70eec261b
11 changed files with 43 additions and 17 deletions

BIN
App/tests/fixtures/Sample_PO.xlsx vendored Normal file

Binary file not shown.

View file

@ -32,7 +32,7 @@ beforeAll(async () => {
const [tech, mgr, vessel, account, vendor] = await Promise.all([ const [tech, mgr, vessel, account, vendor] = await Promise.all([
getSeedUser("tech@pelagia.local"), getSeedUser("tech@pelagia.local"),
getSeedUser("manager@pelagia.local"), getSeedUser("manager@pelagia.local"),
getSeedVessel("MV Ocean Pride"), getSeedVessel("MV Poseidon"),
getSeedAccount("700201"), getSeedAccount("700201"),
getSeedVendor("Apar Industries Ltd"), getSeedVendor("Apar Industries Ltd"),
]); ]);
@ -52,7 +52,11 @@ async function createSubmittedPo(title: string): Promise<string> {
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL")); vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
const form = makePoForm({ title, vesselId, accountId, intent: "submit" }); const form = makePoForm({ title, vesselId, accountId, intent: "submit" });
const result = await createPo(form); const result = await createPo(form);
return (result as { id: string }).id; const id = (result as { id: string }).id;
// Vendor gating: a vendor must be assigned before a PO can be approved.
// Attach the seeded verified vendor directly (test setup) so approval-path tests run.
await db.purchaseOrder.update({ where: { id }, data: { vendorId } });
return id;
} }
// ── M-02: Approve ───────────────────────────────────────────────────────────── // ── M-02: Approve ─────────────────────────────────────────────────────────────

View file

@ -20,6 +20,7 @@ import {
getSeedUser, getSeedUser,
getSeedVessel, getSeedVessel,
getSeedAccount, getSeedAccount,
getSeedVendor,
makePoForm, makePoForm,
deletePosByTitle, deletePosByTitle,
} from "./helpers"; } from "./helpers";
@ -32,20 +33,23 @@ let managerId: string;
let accountsId: string; let accountsId: string;
let vesselId: string; let vesselId: string;
let accountId: string; let accountId: string;
let vendorId: string;
beforeAll(async () => { beforeAll(async () => {
const [tech, mgr, acct, vessel, account] = await Promise.all([ const [tech, mgr, acct, vessel, account, vendor] = await Promise.all([
getSeedUser("tech@pelagia.local"), getSeedUser("tech@pelagia.local"),
getSeedUser("manager@pelagia.local"), getSeedUser("manager@pelagia.local"),
getSeedUser("accounts@pelagia.local"), getSeedUser("accounts@pelagia.local"),
getSeedVessel("MV Sea Breeze"), getSeedVessel("MV Nereid"),
getSeedAccount("700202"), getSeedAccount("700202"),
getSeedVendor("Apar Industries Ltd"),
]); ]);
techId = tech.id; techId = tech.id;
managerId = mgr.id; managerId = mgr.id;
accountsId = acct.id; accountsId = acct.id;
vesselId = vessel.id; vesselId = vessel.id;
accountId = account.id; accountId = account.id;
vendorId = vendor.id;
}); });
afterEach(async () => { afterEach(async () => {
@ -57,6 +61,8 @@ async function createPaidPo(title: string): Promise<string> {
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL")); vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
const form = makePoForm({ title, vesselId, accountId, intent: "submit" }); const form = makePoForm({ title, vesselId, accountId, intent: "submit" });
const { id: poId } = (await createPo(form)) as { id: string }; const { id: poId } = (await createPo(form)) as { id: string };
// Vendor gating: approval requires an assigned vendor.
await db.purchaseOrder.update({ where: { id: poId }, data: { vendorId } });
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER")); vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
await approvePo({ poId }); await approvePo({ poId });

View file

@ -25,7 +25,7 @@ let vendorId: string;
beforeAll(async () => { beforeAll(async () => {
const [tech, vessel, account, vendor] = await Promise.all([ const [tech, vessel, account, vendor] = await Promise.all([
getSeedUser("tech@pelagia.local"), getSeedUser("tech@pelagia.local"),
getSeedVessel("MV Ocean Pride"), getSeedVessel("MV Aegean Wind"),
getSeedAccount("700201"), getSeedAccount("700201"),
getSeedVendor("Apar Industries Ltd"), getSeedVendor("Apar Industries Ltd"),
]); ]);
@ -79,7 +79,7 @@ describe("S-02 — save as draft", () => {
form.set("title", `${PREFIX}NoVessel`); form.set("title", `${PREFIX}NoVessel`);
form.set("accountId", accountId); form.set("accountId", accountId);
form.set("intent", "draft"); form.set("intent", "draft");
form.set("lineItems[0].description", "Item"); form.set("lineItems[0].name", "Item");
form.set("lineItems[0].quantity", "1"); form.set("lineItems[0].quantity", "1");
form.set("lineItems[0].unit", "pc"); form.set("lineItems[0].unit", "pc");
form.set("lineItems[0].unitPrice", "50"); form.set("lineItems[0].unitPrice", "50");

View file

@ -30,7 +30,7 @@ beforeAll(async () => {
getSeedUser("manager@pelagia.local"), getSeedUser("manager@pelagia.local"),
getSeedUser("accounts@pelagia.local"), getSeedUser("accounts@pelagia.local"),
getSeedVessel("MV Pelagia Star"), getSeedVessel("MV Pelagia Star"),
getSeedAccount("TECH-OPS"), getSeedAccount("700201"),
]); ]);
techId = tech.id; techId = tech.id;
managerId = mgr.id; managerId = mgr.id;

View file

@ -46,7 +46,7 @@ export function appendLineItem(
idx: number, idx: number,
item: { description: string; quantity: number; unit: string; unitPrice: number; gstRate?: number } item: { description: string; quantity: number; unit: string; unitPrice: number; gstRate?: number }
) { ) {
form.set(`lineItems[${idx}].description`, item.description); form.set(`lineItems[${idx}].name`, item.description);
form.set(`lineItems[${idx}].quantity`, String(item.quantity)); form.set(`lineItems[${idx}].quantity`, String(item.quantity));
form.set(`lineItems[${idx}].unit`, item.unit); form.set(`lineItems[${idx}].unit`, item.unit);
form.set(`lineItems[${idx}].unitPrice`, String(item.unitPrice)); form.set(`lineItems[${idx}].unitPrice`, String(item.unitPrice));
@ -76,12 +76,22 @@ export function makePoForm(overrides: {
// ── Cleanup helpers ────────────────────────────────────────────────────────── // ── Cleanup helpers ──────────────────────────────────────────────────────────
// POAction has no onDelete: Cascade, so its rows must be removed before the PO.
// (POLineItem / PODocument / Receipt cascade automatically.)
async function deletePosByIds(ids: string[]) {
if (ids.length === 0) return;
await db.pOAction.deleteMany({ where: { poId: { in: ids } } });
await db.purchaseOrder.deleteMany({ where: { id: { in: ids } } });
}
export async function deletePo(poId: string) { export async function deletePo(poId: string) {
await db.purchaseOrder.delete({ where: { id: poId } }).catch(() => {}); await deletePosByIds([poId]).catch(() => {});
} }
export async function deletePosByTitle(titlePrefix: string) { export async function deletePosByTitle(titlePrefix: string) {
await db.purchaseOrder.deleteMany({ const pos = await db.purchaseOrder.findMany({
where: { title: { startsWith: titlePrefix } }, where: { title: { startsWith: titlePrefix } },
select: { id: true },
}); });
await deletePosByIds(pos.map((p) => p.id));
} }

View file

@ -15,7 +15,7 @@ import { POST } from "@/app/api/po/import/route";
import { makeSession, getSeedUser } from "./helpers"; import { makeSession, getSeedUser } from "./helpers";
import type { ParsedImport } from "@/lib/po-import-parser"; import type { ParsedImport } from "@/lib/po-import-parser";
const SAMPLE_XLSX = resolve(__dirname, "../../../../Prototype/Sample_PO.xlsx"); const SAMPLE_XLSX = resolve(__dirname, "../fixtures/Sample_PO.xlsx");
let techId: string; let techId: string;
let managerId: string; let managerId: string;

View file

@ -30,7 +30,7 @@ beforeAll(async () => {
getSeedUser("manager@pelagia.local"), getSeedUser("manager@pelagia.local"),
getSeedUser("accounts@pelagia.local"), getSeedUser("accounts@pelagia.local"),
getSeedVessel("MV Pelagia Star"), getSeedVessel("MV Pelagia Star"),
getSeedAccount("TECH-OPS"), getSeedAccount("700201"),
getSeedVendor("Apar Industries Ltd"), getSeedVendor("Apar Industries Ltd"),
]); ]);
managerId = mgr.id; managerId = mgr.id;

View file

@ -14,7 +14,7 @@ import { createPo } from "@/app/(portal)/po/new/actions";
import { approvePo } from "@/app/(portal)/approvals/[id]/actions"; import { approvePo } from "@/app/(portal)/approvals/[id]/actions";
import { processPayment, markPaid } from "@/app/(portal)/payments/actions"; import { processPayment, markPaid } from "@/app/(portal)/payments/actions";
import { import {
makeSession, getSeedUser, getSeedVessel, getSeedAccount, makeSession, getSeedUser, getSeedVessel, getSeedAccount, getSeedVendor,
makePoForm, deletePosByTitle, makePoForm, deletePosByTitle,
} from "./helpers"; } from "./helpers";
@ -25,20 +25,23 @@ let managerId: string;
let accountsId: string; let accountsId: string;
let vesselId: string; let vesselId: string;
let accountId: string; let accountId: string;
let vendorId: string;
beforeAll(async () => { beforeAll(async () => {
const [tech, mgr, acct, vessel, account] = await Promise.all([ const [tech, mgr, acct, vessel, account, vendor] = await Promise.all([
getSeedUser("tech@pelagia.local"), getSeedUser("tech@pelagia.local"),
getSeedUser("manager@pelagia.local"), getSeedUser("manager@pelagia.local"),
getSeedUser("accounts@pelagia.local"), getSeedUser("accounts@pelagia.local"),
getSeedVessel("MV Sea Breeze"), getSeedVessel("MV Thetis"),
getSeedAccount("700202"), getSeedAccount("700202"),
getSeedVendor("Apar Industries Ltd"),
]); ]);
techId = tech.id; techId = tech.id;
managerId = mgr.id; managerId = mgr.id;
accountsId = acct.id; accountsId = acct.id;
vesselId = vessel.id; vesselId = vessel.id;
accountId = account.id; accountId = account.id;
vendorId = vendor.id;
}); });
afterEach(async () => { afterEach(async () => {
@ -50,6 +53,8 @@ async function createApprovedPo(title: string): Promise<string> {
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL")); vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
const form = makePoForm({ title, vesselId, accountId, intent: "submit" }); const form = makePoForm({ title, vesselId, accountId, intent: "submit" });
const { id: poId } = (await createPo(form)) as { id: string }; const { id: poId } = (await createPo(form)) as { id: string };
// Vendor gating: approval requires an assigned vendor.
await db.purchaseOrder.update({ where: { id: poId }, data: { vendorId } });
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER")); vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
await approvePo({ poId }); await approvePo({ poId });

View file

@ -91,7 +91,8 @@ describe("GET /api/products/search — search behaviour", () => {
it("finds products by product code", async () => { it("finds products by product code", async () => {
const res = await GET(makeRequest("LUBE")); const res = await GET(makeRequest("LUBE"));
const data: { code: string }[] = await res.json(); const data: { code: string }[] = await res.json();
expect(data.every((p) => p.code.toUpperCase().includes("LUBE"))).toBe(true); // search spans code/name/description, so assert the code matches are present (not that every hit is a code match)
expect(data.some((p) => p.code.toUpperCase().includes("LUBE"))).toBe(true);
}); });
it("finds products by description text", async () => { it("finds products by description text", async () => {

View file

@ -39,7 +39,7 @@ beforeAll(async () => {
getSeedUser("manager@pelagia.local"), getSeedUser("manager@pelagia.local"),
getSeedUser("accounts@pelagia.local"), getSeedUser("accounts@pelagia.local"),
getSeedVessel("MV Pelagia Star"), getSeedVessel("MV Pelagia Star"),
getSeedAccount("TECH-OPS"), getSeedAccount("700201"),
getSeedVendor("Apar Industries Ltd"), getSeedVendor("Apar Industries Ltd"),
]); ]);
techId = tech.id; techId = tech.id;