ci: run integration tests on PRs + repair the suite (108/108) #54
11 changed files with 43 additions and 17 deletions
BIN
App/tests/fixtures/Sample_PO.xlsx
vendored
Normal file
BIN
App/tests/fixtures/Sample_PO.xlsx
vendored
Normal file
Binary file not shown.
|
|
@ -32,7 +32,7 @@ beforeAll(async () => {
|
|||
const [tech, mgr, vessel, account, vendor] = await Promise.all([
|
||||
getSeedUser("tech@pelagia.local"),
|
||||
getSeedUser("manager@pelagia.local"),
|
||||
getSeedVessel("MV Ocean Pride"),
|
||||
getSeedVessel("MV Poseidon"),
|
||||
getSeedAccount("700201"),
|
||||
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"));
|
||||
const form = makePoForm({ title, vesselId, accountId, intent: "submit" });
|
||||
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 ─────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
getSeedUser,
|
||||
getSeedVessel,
|
||||
getSeedAccount,
|
||||
getSeedVendor,
|
||||
makePoForm,
|
||||
deletePosByTitle,
|
||||
} from "./helpers";
|
||||
|
|
@ -32,20 +33,23 @@ let managerId: string;
|
|||
let accountsId: string;
|
||||
let vesselId: string;
|
||||
let accountId: string;
|
||||
let vendorId: string;
|
||||
|
||||
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("manager@pelagia.local"),
|
||||
getSeedUser("accounts@pelagia.local"),
|
||||
getSeedVessel("MV Sea Breeze"),
|
||||
getSeedVessel("MV Nereid"),
|
||||
getSeedAccount("700202"),
|
||||
getSeedVendor("Apar Industries Ltd"),
|
||||
]);
|
||||
techId = tech.id;
|
||||
managerId = mgr.id;
|
||||
accountsId = acct.id;
|
||||
vesselId = vessel.id;
|
||||
accountId = account.id;
|
||||
vendorId = vendor.id;
|
||||
});
|
||||
|
||||
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"));
|
||||
const form = makePoForm({ title, vesselId, accountId, intent: "submit" });
|
||||
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"));
|
||||
await approvePo({ poId });
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ let vendorId: string;
|
|||
beforeAll(async () => {
|
||||
const [tech, vessel, account, vendor] = await Promise.all([
|
||||
getSeedUser("tech@pelagia.local"),
|
||||
getSeedVessel("MV Ocean Pride"),
|
||||
getSeedVessel("MV Aegean Wind"),
|
||||
getSeedAccount("700201"),
|
||||
getSeedVendor("Apar Industries Ltd"),
|
||||
]);
|
||||
|
|
@ -79,7 +79,7 @@ describe("S-02 — save as draft", () => {
|
|||
form.set("title", `${PREFIX}NoVessel`);
|
||||
form.set("accountId", accountId);
|
||||
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].unit", "pc");
|
||||
form.set("lineItems[0].unitPrice", "50");
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ beforeAll(async () => {
|
|||
getSeedUser("manager@pelagia.local"),
|
||||
getSeedUser("accounts@pelagia.local"),
|
||||
getSeedVessel("MV Pelagia Star"),
|
||||
getSeedAccount("TECH-OPS"),
|
||||
getSeedAccount("700201"),
|
||||
]);
|
||||
techId = tech.id;
|
||||
managerId = mgr.id;
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export function appendLineItem(
|
|||
idx: 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}].unit`, item.unit);
|
||||
form.set(`lineItems[${idx}].unitPrice`, String(item.unitPrice));
|
||||
|
|
@ -76,12 +76,22 @@ export function makePoForm(overrides: {
|
|||
|
||||
// ── 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) {
|
||||
await db.purchaseOrder.delete({ where: { id: poId } }).catch(() => {});
|
||||
await deletePosByIds([poId]).catch(() => {});
|
||||
}
|
||||
|
||||
export async function deletePosByTitle(titlePrefix: string) {
|
||||
await db.purchaseOrder.deleteMany({
|
||||
const pos = await db.purchaseOrder.findMany({
|
||||
where: { title: { startsWith: titlePrefix } },
|
||||
select: { id: true },
|
||||
});
|
||||
await deletePosByIds(pos.map((p) => p.id));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ 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");
|
||||
const SAMPLE_XLSX = resolve(__dirname, "../fixtures/Sample_PO.xlsx");
|
||||
|
||||
let techId: string;
|
||||
let managerId: string;
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ beforeAll(async () => {
|
|||
getSeedUser("manager@pelagia.local"),
|
||||
getSeedUser("accounts@pelagia.local"),
|
||||
getSeedVessel("MV Pelagia Star"),
|
||||
getSeedAccount("TECH-OPS"),
|
||||
getSeedAccount("700201"),
|
||||
getSeedVendor("Apar Industries Ltd"),
|
||||
]);
|
||||
managerId = mgr.id;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { createPo } from "@/app/(portal)/po/new/actions";
|
|||
import { approvePo } from "@/app/(portal)/approvals/[id]/actions";
|
||||
import { processPayment, markPaid } from "@/app/(portal)/payments/actions";
|
||||
import {
|
||||
makeSession, getSeedUser, getSeedVessel, getSeedAccount,
|
||||
makeSession, getSeedUser, getSeedVessel, getSeedAccount, getSeedVendor,
|
||||
makePoForm, deletePosByTitle,
|
||||
} from "./helpers";
|
||||
|
||||
|
|
@ -25,20 +25,23 @@ let managerId: string;
|
|||
let accountsId: string;
|
||||
let vesselId: string;
|
||||
let accountId: string;
|
||||
let vendorId: string;
|
||||
|
||||
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("manager@pelagia.local"),
|
||||
getSeedUser("accounts@pelagia.local"),
|
||||
getSeedVessel("MV Sea Breeze"),
|
||||
getSeedVessel("MV Thetis"),
|
||||
getSeedAccount("700202"),
|
||||
getSeedVendor("Apar Industries Ltd"),
|
||||
]);
|
||||
techId = tech.id;
|
||||
managerId = mgr.id;
|
||||
accountsId = acct.id;
|
||||
vesselId = vessel.id;
|
||||
accountId = account.id;
|
||||
vendorId = vendor.id;
|
||||
});
|
||||
|
||||
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"));
|
||||
const form = makePoForm({ title, vesselId, accountId, intent: "submit" });
|
||||
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"));
|
||||
await approvePo({ poId });
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@ describe("GET /api/products/search — search behaviour", () => {
|
|||
it("finds products by product code", async () => {
|
||||
const res = await GET(makeRequest("LUBE"));
|
||||
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 () => {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ beforeAll(async () => {
|
|||
getSeedUser("manager@pelagia.local"),
|
||||
getSeedUser("accounts@pelagia.local"),
|
||||
getSeedVessel("MV Pelagia Star"),
|
||||
getSeedAccount("TECH-OPS"),
|
||||
getSeedAccount("700201"),
|
||||
getSeedVendor("Apar Industries Ltd"),
|
||||
]);
|
||||
techId = tech.id;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue