Merge branch 'master' into feat/po-cancel-supersede
This commit is contained in:
commit
a8d772d63b
12 changed files with 104 additions and 27 deletions
|
|
@ -4,6 +4,7 @@ name: PR checks
|
||||||
# - code changes must ship with tests (docs/config/automation are exempt)
|
# - code changes must ship with tests (docs/config/automation are exempt)
|
||||||
# - type-check is clean across the whole project (tests included)
|
# - type-check is clean across the whole project (tests included)
|
||||||
# - unit tests pass
|
# - unit tests pass
|
||||||
|
# - integration tests pass against an ephemeral Postgres (migrate + seed)
|
||||||
# Runs on the pms1 host runner. See automation/README.md > "Contribution policy".
|
# Runs on the pms1 host runner. See automation/README.md > "Contribution policy".
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|
@ -56,3 +57,45 @@ jobs:
|
||||||
set -e
|
set -e
|
||||||
export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"
|
export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"
|
||||||
cd App && pnpm test # jsdom unit tests, no DB — must pass
|
cd App && pnpm test # jsdom unit tests, no DB — must pass
|
||||||
|
|
||||||
|
integration:
|
||||||
|
runs-on: host
|
||||||
|
steps:
|
||||||
|
- name: Checkout PR
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Integration tests (ephemeral Postgres)
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"
|
||||||
|
|
||||||
|
# Throwaway Postgres per run — isolated from prod / pelagia_test / staging.
|
||||||
|
# A random host port avoids collisions with the host DB and concurrent runs.
|
||||||
|
PG="ci-pg-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT:-1}"
|
||||||
|
cleanup() { docker rm -f "$PG" >/dev/null 2>&1 || true; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
docker rm -f "$PG" >/dev/null 2>&1 || true
|
||||||
|
docker run -d --name "$PG" \
|
||||||
|
-e POSTGRES_USER=ci -e POSTGRES_PASSWORD=ci -e POSTGRES_DB=pelagia_ci \
|
||||||
|
-p 127.0.0.1::5432 postgres:16 >/dev/null
|
||||||
|
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
docker exec "$PG" pg_isready -U ci -d pelagia_ci >/dev/null 2>&1 && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
PORT=$(docker inspect --format '{{ (index (index .NetworkSettings.Ports "5432/tcp") 0).HostPort }}' "$PG")
|
||||||
|
export DATABASE_URL="postgresql://ci:ci@127.0.0.1:${PORT}/pelagia_ci"
|
||||||
|
# Non-secret placeholders so auth.ts (reads these at module load) boots in dev mode.
|
||||||
|
export NEXTAUTH_SECRET="ci-secret"
|
||||||
|
export NEXTAUTH_URL="http://localhost:3000"
|
||||||
|
export AZURE_AD_CLIENT_ID="placeholder"
|
||||||
|
export AZURE_AD_CLIENT_SECRET="placeholder"
|
||||||
|
export AZURE_AD_TENANT_ID="placeholder"
|
||||||
|
|
||||||
|
cd App
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm db:generate
|
||||||
|
pnpm db:migrate:deploy # apply migrations to the fresh DB
|
||||||
|
pnpm db:seed # dev seed — integration tests rely on it
|
||||||
|
pnpm test:integration # node + real DB — must pass
|
||||||
|
|
|
||||||
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([
|
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 ─────────────────────────────────────────────────────────────
|
||||||
|
|
@ -340,7 +344,7 @@ describe("S-07 — edit and resubmit after edits requested", () => {
|
||||||
await requestEdits({ poId, note: "Update line items" });
|
await requestEdits({ poId, note: "Update line items" });
|
||||||
|
|
||||||
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: `${PREFIX}Resubmit`, vesselId, accountId, intent: "submit" });
|
const form = makePoForm({ title: `${PREFIX}Resubmit`, vesselId, accountId, intent: "resubmit" });
|
||||||
const result = await updatePo(poId, form);
|
const result = await updatePo(poId, form);
|
||||||
expect(result).toEqual({ id: poId });
|
expect(result).toEqual({ id: poId });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
@ -58,7 +58,7 @@ export function makePoForm(overrides: {
|
||||||
vesselId: string;
|
vesselId: string;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
vendorId?: string;
|
vendorId?: string;
|
||||||
intent?: "draft" | "submit";
|
intent?: "draft" | "submit" | "resubmit";
|
||||||
lineItems?: Array<{ description: string; quantity: number; unit: string; unitPrice: number; gstRate?: number }>;
|
lineItems?: Array<{ description: string; quantity: number; unit: string; unitPrice: number; gstRate?: number }>;
|
||||||
}): FormData {
|
}): FormData {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
|
|
@ -76,12 +76,23 @@ export function makePoForm(overrides: {
|
||||||
|
|
||||||
// ── Cleanup helpers ──────────────────────────────────────────────────────────
|
// ── Cleanup helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// POAction and Receipt have no onDelete: Cascade, so their rows must be removed
|
||||||
|
// before the PO. (POLineItem / PODocument cascade automatically.)
|
||||||
|
async function deletePosByIds(ids: string[]) {
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
await db.pOAction.deleteMany({ where: { poId: { in: ids } } });
|
||||||
|
await db.receipt.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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
@ -146,14 +151,14 @@ describe("A-02 — mark PO as paid with reference number", () => {
|
||||||
expect(calls).toContain("PAYMENT_SENT");
|
expect(calls).toContain("PAYMENT_SENT");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("MANAGER role cannot mark as paid (wrong permission)", async () => {
|
it("TECHNICAL role cannot mark as paid (no process_payment permission)", async () => {
|
||||||
const poId = await createApprovedPo(`${PREFIX}PaidMgrForbidden`);
|
const poId = await createApprovedPo(`${PREFIX}PaidTechForbidden`);
|
||||||
|
|
||||||
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
||||||
await processPayment({ poId });
|
await processPayment({ poId });
|
||||||
|
|
||||||
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
const result = await markPaid({ poId, paymentRef: "MGR-REF", paymentDate: TODAY });
|
const result = await markPaid({ poId, paymentRef: "TECH-REF", paymentDate: TODAY });
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* - Unverified vendor rejected by provideVendorId
|
* - Unverified vendor rejected by provideVendorId
|
||||||
* - AUDITOR cannot provide vendor ID
|
* - AUDITOR cannot provide vendor ID
|
||||||
*/
|
*/
|
||||||
import { vi, describe, it, expect, beforeAll, afterEach } from "vitest";
|
import { vi, describe, it, expect, beforeAll, afterAll, afterEach } from "vitest";
|
||||||
|
|
||||||
vi.mock("@/auth", () => ({ auth: vi.fn() }));
|
vi.mock("@/auth", () => ({ auth: vi.fn() }));
|
||||||
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -66,15 +66,22 @@ beforeAll(async () => {
|
||||||
auditorId = created.id;
|
auditorId = created.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab an unverified vendor
|
// A vendor with no formal vendorId code — provideVendorId must reject it.
|
||||||
const unverified = await db.vendor.findFirst({ where: { isVerified: false } });
|
// (Seeded "unverified" vendors can still carry a code, so create a code-less one.)
|
||||||
unverifiedVendorDbId = unverified!.id;
|
const noCode = await db.vendor.create({
|
||||||
|
data: { name: `${PREFIX}NoCodeVendor`, isVerified: false, vendorId: null },
|
||||||
|
});
|
||||||
|
unverifiedVendorDbId = noCode.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await deletePosByTitle(PREFIX);
|
await deletePosByTitle(PREFIX);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await db.vendor.deleteMany({ where: { name: { startsWith: PREFIX } } });
|
||||||
|
});
|
||||||
|
|
||||||
async function makeReviewPo(title: string, withVendor = false) {
|
async function makeReviewPo(title: string, withVendor = false) {
|
||||||
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({
|
const form = makePoForm({
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue