test+ci: green the test baseline and make type-check + unit tests hard gates
All checks were successful
PR checks / checks (pull_request) Successful in 30s
All checks were successful
PR checks / checks (pull_request) Successful in 30s
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>
This commit is contained in:
parent
debac55a8a
commit
938ff6df89
14 changed files with 134 additions and 134 deletions
|
|
@ -8,10 +8,10 @@
|
||||||
|
|
||||||
- [ ] **Tests** added or updated for this change — or it is a docs/config/automation-only PR (tests not applicable). Model: the integration test on `claude/issue-12` (prod-mirror DB, raw-SQL inserts, prefix-isolated, cleans up after itself).
|
- [ ] **Tests** added or updated for this change — or it is a docs/config/automation-only PR (tests not applicable). Model: the integration test on `claude/issue-12` (prod-mirror DB, raw-SQL inserts, prefix-isolated, cleans up after itself).
|
||||||
- [ ] **Docs** updated where relevant (App/README.md, App/CLAUDE.md, Docs/, automation/README.md, CHANGELOG.md).
|
- [ ] **Docs** updated where relevant (App/README.md, App/CLAUDE.md, Docs/, automation/README.md, CHANGELOG.md).
|
||||||
- [ ] `pnpm type-check` shows no new errors in application code.
|
- [ ] `pnpm type-check` is clean and `pnpm test` passes (the PR check enforces both).
|
||||||
- [ ] Verified the change (how: unit/integration tests, or a dev server on port 3100 against the test DB).
|
- [ ] Verified the change (how: unit/integration tests, or a dev server on port 3100 against the test DB).
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The "PR checks" workflow enforces the test-presence rule automatically:
|
The "PR checks" workflow runs on every PR and hard-fails on: a code change with no
|
||||||
a PR that touches App/app|lib|components|hooks without any test change will fail.
|
test change, any type-check error, or any failing unit test.
|
||||||
-->
|
-->
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
name: PR checks
|
name: PR checks
|
||||||
|
|
||||||
# Enforces the contribution policy on every PR into master:
|
# Enforces the contribution policy on every PR into master (all gates hard):
|
||||||
# - code changes must ship with tests (docs/config/automation are exempt)
|
# - code changes must ship with tests (docs/config/automation are exempt)
|
||||||
# - no new type errors in application code
|
# - type-check is clean across the whole project (tests included)
|
||||||
# - unit tests are reported (advisory until the baseline is green)
|
# - unit tests pass
|
||||||
# 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:
|
||||||
|
|
@ -42,25 +42,17 @@ jobs:
|
||||||
fi
|
fi
|
||||||
echo "OK — test-presence policy satisfied."
|
echo "OK — test-presence policy satisfied."
|
||||||
|
|
||||||
- name: No new type errors in application code
|
- name: Type-check (no errors)
|
||||||
run: |
|
run: |
|
||||||
set -uo pipefail
|
set -e
|
||||||
export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"
|
export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"
|
||||||
cd App
|
cd App
|
||||||
pnpm install --frozen-lockfile
|
pnpm install --frozen-lockfile
|
||||||
pnpm db:generate # prisma client types (no DB needed)
|
pnpm db:generate # prisma client types (no DB connection needed)
|
||||||
pnpm type-check > /tmp/tc.txt 2>&1 || true
|
pnpm type-check # whole project, tests included — must be clean
|
||||||
# Gate on application-code errors only; the test suite has a known
|
|
||||||
# pre-existing type-mismatch baseline that is tracked separately.
|
|
||||||
app_errors=$(grep -E 'error TS' /tmp/tc.txt | grep -vE '/tests/|\.test\.|\.spec\.' || true)
|
|
||||||
if [ -n "$app_errors" ]; then
|
|
||||||
echo "::error::Type errors in application code:"; printf '%s\n' "$app_errors"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "OK — no type errors in application code."
|
|
||||||
|
|
||||||
- name: Unit tests (advisory)
|
- name: Unit tests
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
run: |
|
||||||
|
set -e
|
||||||
export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"
|
export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"
|
||||||
cd App && pnpm test
|
cd App && pnpm test # jsdom unit tests, no DB — must pass
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ afterEach(async () => {
|
||||||
|
|
||||||
// Helper: create a PO in MGR_REVIEW state
|
// Helper: create a PO in MGR_REVIEW state
|
||||||
async function createSubmittedPo(title: string): Promise<string> {
|
async function createSubmittedPo(title: string): Promise<string> {
|
||||||
vi.mocked(auth).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;
|
return (result as { id: string }).id;
|
||||||
|
|
@ -60,7 +60,7 @@ async function createSubmittedPo(title: string): Promise<string> {
|
||||||
describe("M-02 — approve PO", () => {
|
describe("M-02 — approve PO", () => {
|
||||||
it("transitions PO from MGR_REVIEW to MGR_APPROVED", async () => {
|
it("transitions PO from MGR_REVIEW to MGR_APPROVED", async () => {
|
||||||
const poId = await createSubmittedPo(`${PREFIX}Approve`);
|
const poId = await createSubmittedPo(`${PREFIX}Approve`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
|
|
||||||
const result = await approvePo({ poId });
|
const result = await approvePo({ poId });
|
||||||
expect(result).toEqual({ ok: true });
|
expect(result).toEqual({ ok: true });
|
||||||
|
|
@ -72,7 +72,7 @@ describe("M-02 — approve PO", () => {
|
||||||
|
|
||||||
it("stores managerNote when approving with note", async () => {
|
it("stores managerNote when approving with note", async () => {
|
||||||
const poId = await createSubmittedPo(`${PREFIX}ApproveNote`);
|
const poId = await createSubmittedPo(`${PREFIX}ApproveNote`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
|
|
||||||
await approvePo({ poId, note: "Approved — expedite delivery", withNote: true });
|
await approvePo({ poId, note: "Approved — expedite delivery", withNote: true });
|
||||||
|
|
||||||
|
|
@ -88,7 +88,7 @@ describe("M-02 — approve PO", () => {
|
||||||
const { notify } = await import("@/lib/notifier");
|
const { notify } = await import("@/lib/notifier");
|
||||||
vi.mocked(notify).mockClear();
|
vi.mocked(notify).mockClear();
|
||||||
const poId = await createSubmittedPo(`${PREFIX}ApproveNotify`);
|
const poId = await createSubmittedPo(`${PREFIX}ApproveNotify`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
|
|
||||||
await approvePo({ poId });
|
await approvePo({ poId });
|
||||||
expect(vi.mocked(notify)).toHaveBeenCalledWith(
|
expect(vi.mocked(notify)).toHaveBeenCalledWith(
|
||||||
|
|
@ -98,18 +98,18 @@ describe("M-02 — approve PO", () => {
|
||||||
|
|
||||||
it("returns error when TECHNICAL role tries to approve", async () => {
|
it("returns error when TECHNICAL role tries to approve", async () => {
|
||||||
const poId = await createSubmittedPo(`${PREFIX}ApproveForbidden`);
|
const poId = await createSubmittedPo(`${PREFIX}ApproveForbidden`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
const result = await approvePo({ poId });
|
const result = await approvePo({ poId });
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns error when PO is not in MGR_REVIEW state", async () => {
|
it("returns error when PO is not in MGR_REVIEW state", async () => {
|
||||||
// Create a DRAFT PO, don't submit
|
// Create a DRAFT PO, don't submit
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
const form = makePoForm({ title: `${PREFIX}ApproveDraft`, vesselId, accountId, intent: "draft" });
|
const form = makePoForm({ title: `${PREFIX}ApproveDraft`, vesselId, accountId, intent: "draft" });
|
||||||
const { id: poId } = (await createPo(form)) as { id: string };
|
const { id: poId } = (await createPo(form)) as { id: string };
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
const result = await approvePo({ poId });
|
const result = await approvePo({ poId });
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
});
|
});
|
||||||
|
|
@ -120,7 +120,7 @@ describe("M-02 — approve PO", () => {
|
||||||
describe("M-03 — reject PO", () => {
|
describe("M-03 — reject PO", () => {
|
||||||
it("transitions PO from MGR_REVIEW to REJECTED with note", async () => {
|
it("transitions PO from MGR_REVIEW to REJECTED with note", async () => {
|
||||||
const poId = await createSubmittedPo(`${PREFIX}Reject`);
|
const poId = await createSubmittedPo(`${PREFIX}Reject`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
|
|
||||||
const result = await rejectPo({ poId, note: "Budget exceeded for this quarter" });
|
const result = await rejectPo({ poId, note: "Budget exceeded for this quarter" });
|
||||||
expect(result).toEqual({ ok: true });
|
expect(result).toEqual({ ok: true });
|
||||||
|
|
@ -132,7 +132,7 @@ describe("M-03 — reject PO", () => {
|
||||||
|
|
||||||
it("creates a REJECTED action entry in the audit trail", async () => {
|
it("creates a REJECTED action entry in the audit trail", async () => {
|
||||||
const poId = await createSubmittedPo(`${PREFIX}RejectAudit`);
|
const poId = await createSubmittedPo(`${PREFIX}RejectAudit`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
await rejectPo({ poId, note: "Not needed" });
|
await rejectPo({ poId, note: "Not needed" });
|
||||||
|
|
||||||
const action = await db.pOAction.findFirst({ where: { poId, actionType: "REJECTED" } });
|
const action = await db.pOAction.findFirst({ where: { poId, actionType: "REJECTED" } });
|
||||||
|
|
@ -143,7 +143,7 @@ describe("M-03 — reject PO", () => {
|
||||||
const { notify } = await import("@/lib/notifier");
|
const { notify } = await import("@/lib/notifier");
|
||||||
vi.mocked(notify).mockClear();
|
vi.mocked(notify).mockClear();
|
||||||
const poId = await createSubmittedPo(`${PREFIX}RejectNotify`);
|
const poId = await createSubmittedPo(`${PREFIX}RejectNotify`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
|
|
||||||
await rejectPo({ poId, note: "See notes" });
|
await rejectPo({ poId, note: "See notes" });
|
||||||
expect(vi.mocked(notify)).toHaveBeenCalledWith(
|
expect(vi.mocked(notify)).toHaveBeenCalledWith(
|
||||||
|
|
@ -157,7 +157,7 @@ describe("M-03 — reject PO", () => {
|
||||||
describe("M-04 — request edits", () => {
|
describe("M-04 — request edits", () => {
|
||||||
it("transitions PO to EDITS_REQUESTED with manager note", async () => {
|
it("transitions PO to EDITS_REQUESTED with manager note", async () => {
|
||||||
const poId = await createSubmittedPo(`${PREFIX}Edits`);
|
const poId = await createSubmittedPo(`${PREFIX}Edits`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
|
|
||||||
const result = await requestEdits({ poId, note: "Please add vendor ID" });
|
const result = await requestEdits({ poId, note: "Please add vendor ID" });
|
||||||
expect(result).toEqual({ ok: true });
|
expect(result).toEqual({ ok: true });
|
||||||
|
|
@ -173,7 +173,7 @@ describe("M-04 — request edits", () => {
|
||||||
describe("M-04 — request vendor ID", () => {
|
describe("M-04 — request vendor ID", () => {
|
||||||
it("transitions PO to VENDOR_ID_PENDING", async () => {
|
it("transitions PO to VENDOR_ID_PENDING", async () => {
|
||||||
const poId = await createSubmittedPo(`${PREFIX}VendorIdReq`);
|
const poId = await createSubmittedPo(`${PREFIX}VendorIdReq`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
|
|
||||||
const result = await requestVendorId({ poId });
|
const result = await requestVendorId({ poId });
|
||||||
expect(result).toEqual({ ok: true });
|
expect(result).toEqual({ ok: true });
|
||||||
|
|
@ -188,10 +188,10 @@ describe("M-04 — request vendor ID", () => {
|
||||||
describe("S-06 — provide vendor ID", () => {
|
describe("S-06 — provide vendor ID", () => {
|
||||||
it("transitions VENDOR_ID_PENDING back to MGR_REVIEW", async () => {
|
it("transitions VENDOR_ID_PENDING back to MGR_REVIEW", async () => {
|
||||||
const poId = await createSubmittedPo(`${PREFIX}ProvideVendor`);
|
const poId = await createSubmittedPo(`${PREFIX}ProvideVendor`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
await requestVendorId({ poId });
|
await requestVendorId({ poId });
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
const result = await provideVendorId({ poId, vendorId });
|
const result = await provideVendorId({ poId, vendorId });
|
||||||
expect(result).toEqual({ ok: true });
|
expect(result).toEqual({ ok: true });
|
||||||
|
|
||||||
|
|
@ -242,7 +242,7 @@ describe("inventory — updated at MGR_APPROVED, not at closure", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
const result = await approvePo({ poId: po.id });
|
const result = await approvePo({ poId: po.id });
|
||||||
expect(result).toEqual({ ok: true });
|
expect(result).toEqual({ ok: true });
|
||||||
|
|
||||||
|
|
@ -285,7 +285,7 @@ describe("inventory — updated at MGR_APPROVED, not at closure", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
await approvePo({ poId: po.id });
|
await approvePo({ poId: po.id });
|
||||||
|
|
||||||
const countAfter = await db.itemInventory.count({ where: { siteId: site.id } });
|
const countAfter = await db.itemInventory.count({ where: { siteId: site.id } });
|
||||||
|
|
@ -322,7 +322,7 @@ describe("inventory — updated at MGR_APPROVED, not at closure", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
await approvePo({ poId: po.id });
|
await approvePo({ poId: po.id });
|
||||||
|
|
||||||
const totalAfter = await db.itemInventory.count();
|
const totalAfter = await db.itemInventory.count();
|
||||||
|
|
@ -336,11 +336,11 @@ describe("S-07 — edit and resubmit after edits requested", () => {
|
||||||
it("resubmitting from EDITS_REQUESTED transitions to MGR_REVIEW", async () => {
|
it("resubmitting from EDITS_REQUESTED transitions to MGR_REVIEW", async () => {
|
||||||
const poId = await createSubmittedPo(`${PREFIX}Resubmit`);
|
const poId = await createSubmittedPo(`${PREFIX}Resubmit`);
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
await requestEdits({ poId, note: "Update line items" });
|
await requestEdits({ poId, note: "Update line items" });
|
||||||
|
|
||||||
vi.mocked(auth).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: "resubmit" });
|
const form = makePoForm({ title: `${PREFIX}Resubmit`, vesselId, accountId, intent: "submit" });
|
||||||
const result = await updatePo(poId, form);
|
const result = await updatePo(poId, form);
|
||||||
expect(result).toEqual({ id: poId });
|
expect(result).toEqual({ id: poId });
|
||||||
|
|
||||||
|
|
@ -350,11 +350,11 @@ describe("S-07 — edit and resubmit after edits requested", () => {
|
||||||
|
|
||||||
it("saving edits without resubmitting stays as DRAFT (save intent)", async () => {
|
it("saving edits without resubmitting stays as DRAFT (save intent)", async () => {
|
||||||
// Create a DRAFT PO
|
// Create a DRAFT PO
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
const form = makePoForm({ title: `${PREFIX}SaveDraft`, vesselId, accountId, intent: "draft" });
|
const form = makePoForm({ title: `${PREFIX}SaveDraft`, vesselId, accountId, intent: "draft" });
|
||||||
const { id: poId } = (await createPo(form)) as { id: string };
|
const { id: poId } = (await createPo(form)) as { id: string };
|
||||||
|
|
||||||
const editForm = makePoForm({ title: `${PREFIX}SaveDraft`, vesselId, accountId, intent: "save" });
|
const editForm = makePoForm({ title: `${PREFIX}SaveDraft`, vesselId, accountId, intent: "draft" });
|
||||||
const result = await updatePo(poId, editForm);
|
const result = await updatePo(poId, editForm);
|
||||||
expect(result).toEqual({ id: poId });
|
expect(result).toEqual({ id: poId });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,18 +54,18 @@ afterEach(async () => {
|
||||||
|
|
||||||
/** Create a PO and drive it to PAID_DELIVERED (fully paid). */
|
/** Create a PO and drive it to PAID_DELIVERED (fully paid). */
|
||||||
async function createPaidPo(title: string): Promise<string> {
|
async function createPaidPo(title: string): Promise<string> {
|
||||||
vi.mocked(auth).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 };
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
await approvePo({ poId });
|
await approvePo({ poId });
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
||||||
await processPayment({ poId });
|
await processPayment({ poId });
|
||||||
await markPaid({ poId, paymentRef: "NEFT-TEST-RECEIPT", paymentDate: TODAY });
|
await markPaid({ poId, paymentRef: "NEFT-TEST-RECEIPT", paymentDate: TODAY });
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
return poId;
|
return poId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,7 +167,7 @@ describe("confirmReceipt — permission guards", () => {
|
||||||
const otherTech = await getSeedUser("tech@pelagia.local");
|
const otherTech = await getSeedUser("tech@pelagia.local");
|
||||||
// Use a different user id to simulate a different submitter
|
// Use a different user id to simulate a different submitter
|
||||||
const fakeSession = makeSession(managerId, "TECHNICAL");
|
const fakeSession = makeSession(managerId, "TECHNICAL");
|
||||||
vi.mocked(auth).mockResolvedValue(fakeSession);
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(fakeSession);
|
||||||
|
|
||||||
const result = await confirmReceipt({ poId });
|
const result = await confirmReceipt({ poId });
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
|
|
@ -176,7 +176,7 @@ describe("confirmReceipt — permission guards", () => {
|
||||||
|
|
||||||
it("rejects confirmation on a PO in wrong status", async () => {
|
it("rejects confirmation on a PO in wrong status", async () => {
|
||||||
// Create a PO that is still DRAFT (no payment yet)
|
// Create a PO that is still DRAFT (no payment yet)
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
const form = makePoForm({ title: `${PREFIX}WrongStatus`, vesselId, accountId, intent: "draft" });
|
const form = makePoForm({ title: `${PREFIX}WrongStatus`, vesselId, accountId, intent: "draft" });
|
||||||
const { id: poId } = (await createPo(form)) as { id: string };
|
const { id: poId } = (await createPo(form)) as { id: string };
|
||||||
|
|
||||||
|
|
@ -190,7 +190,7 @@ describe("confirmReceipt — permission guards", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns error when not authenticated", async () => {
|
it("returns error when not authenticated", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(null);
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(null);
|
||||||
const result = await confirmReceipt({ poId: "any-id" });
|
const result = await confirmReceipt({ poId: "any-id" });
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ afterEach(async () => {
|
||||||
|
|
||||||
describe("S-02 — save as draft", () => {
|
describe("S-02 — save as draft", () => {
|
||||||
it("creates a PO in DRAFT status", async () => {
|
it("creates a PO in DRAFT status", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
|
|
||||||
const form = makePoForm({
|
const form = makePoForm({
|
||||||
title: `${PREFIX}Draft`,
|
title: `${PREFIX}Draft`,
|
||||||
|
|
@ -59,7 +59,7 @@ describe("S-02 — save as draft", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns error for unauthenticated request", async () => {
|
it("returns error for unauthenticated request", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(null);
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(null);
|
||||||
const form = makePoForm({ title: `${PREFIX}Unauth`, vesselId, accountId });
|
const form = makePoForm({ title: `${PREFIX}Unauth`, vesselId, accountId });
|
||||||
const result = await createPo(form);
|
const result = await createPo(form);
|
||||||
expect(result).toEqual({ error: "Unauthorized" });
|
expect(result).toEqual({ error: "Unauthorized" });
|
||||||
|
|
@ -67,14 +67,14 @@ describe("S-02 — save as draft", () => {
|
||||||
|
|
||||||
it("returns error when ACCOUNTS role tries to create a PO", async () => {
|
it("returns error when ACCOUNTS role tries to create a PO", async () => {
|
||||||
const acct = await getSeedUser("accounts@pelagia.local");
|
const acct = await getSeedUser("accounts@pelagia.local");
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(acct.id, "ACCOUNTS"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(acct.id, "ACCOUNTS"));
|
||||||
const form = makePoForm({ title: `${PREFIX}ForbiddenAccts`, vesselId, accountId });
|
const form = makePoForm({ title: `${PREFIX}ForbiddenAccts`, vesselId, accountId });
|
||||||
const result = await createPo(form);
|
const result = await createPo(form);
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns error when a required field (vesselId) is missing", async () => {
|
it("returns error when a required field (vesselId) is missing", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.set("title", `${PREFIX}NoVessel`);
|
form.set("title", `${PREFIX}NoVessel`);
|
||||||
form.set("accountId", accountId);
|
form.set("accountId", accountId);
|
||||||
|
|
@ -93,7 +93,7 @@ describe("S-02 — save as draft", () => {
|
||||||
|
|
||||||
describe("S-01 — create PO with line items", () => {
|
describe("S-01 — create PO with line items", () => {
|
||||||
it("stores line items with correct quantity, unit price, and GST rate", async () => {
|
it("stores line items with correct quantity, unit price, and GST rate", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
|
|
||||||
const form = makePoForm({
|
const form = makePoForm({
|
||||||
title: `${PREFIX}LineItems`,
|
title: `${PREFIX}LineItems`,
|
||||||
|
|
@ -120,7 +120,7 @@ describe("S-01 — create PO with line items", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets totalAmount to grand total including GST", async () => {
|
it("sets totalAmount to grand total including GST", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
|
|
||||||
// 10 × 100 × 1.18 = 1180
|
// 10 × 100 × 1.18 = 1180
|
||||||
const form = makePoForm({
|
const form = makePoForm({
|
||||||
|
|
@ -135,7 +135,7 @@ describe("S-01 — create PO with line items", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stores optional fields (PI quotation no, place of delivery, TC fields)", async () => {
|
it("stores optional fields (PI quotation no, place of delivery, TC fields)", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
|
|
||||||
const form = makePoForm({ title: `${PREFIX}Optional`, vesselId, accountId, intent: "draft" });
|
const form = makePoForm({ title: `${PREFIX}Optional`, vesselId, accountId, intent: "draft" });
|
||||||
form.set("piQuotationNo", "Verbal");
|
form.set("piQuotationNo", "Verbal");
|
||||||
|
|
@ -154,7 +154,7 @@ describe("S-01 — create PO with line items", () => {
|
||||||
|
|
||||||
it("allows MANNING role to create a PO", async () => {
|
it("allows MANNING role to create a PO", async () => {
|
||||||
const manning = await getSeedUser("manning@pelagia.local");
|
const manning = await getSeedUser("manning@pelagia.local");
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(manning.id, "MANNING"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(manning.id, "MANNING"));
|
||||||
const form = makePoForm({ title: `${PREFIX}Manning`, vesselId, accountId });
|
const form = makePoForm({ title: `${PREFIX}Manning`, vesselId, accountId });
|
||||||
const result = await createPo(form);
|
const result = await createPo(form);
|
||||||
expect(result).not.toHaveProperty("error");
|
expect(result).not.toHaveProperty("error");
|
||||||
|
|
@ -165,7 +165,7 @@ describe("S-01 — create PO with line items", () => {
|
||||||
|
|
||||||
describe("S-03 — submit for approval", () => {
|
describe("S-03 — submit for approval", () => {
|
||||||
it("creates PO with status MGR_REVIEW and sets submittedAt", async () => {
|
it("creates PO with status MGR_REVIEW and sets submittedAt", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
|
|
||||||
const form = makePoForm({ title: `${PREFIX}Submit`, vesselId, accountId, intent: "submit" });
|
const form = makePoForm({ title: `${PREFIX}Submit`, vesselId, accountId, intent: "submit" });
|
||||||
const result = await createPo(form);
|
const result = await createPo(form);
|
||||||
|
|
@ -180,7 +180,7 @@ describe("S-03 — submit for approval", () => {
|
||||||
it("sends notification to managers on submit", async () => {
|
it("sends notification to managers on submit", async () => {
|
||||||
const { notify } = await import("@/lib/notifier");
|
const { notify } = await import("@/lib/notifier");
|
||||||
vi.mocked(notify).mockClear();
|
vi.mocked(notify).mockClear();
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
|
|
||||||
const form = makePoForm({ title: `${PREFIX}Notify`, vesselId, accountId, intent: "submit" });
|
const form = makePoForm({ title: `${PREFIX}Notify`, vesselId, accountId, intent: "submit" });
|
||||||
await createPo(form);
|
await createPo(form);
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ afterEach(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function createDraft(title: string, asUserId = techId, asRole: Parameters<typeof makeSession>[1] = "TECHNICAL") {
|
async function createDraft(title: string, asUserId = techId, asRole: Parameters<typeof makeSession>[1] = "TECHNICAL") {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(asUserId, asRole));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(asUserId, asRole));
|
||||||
const form = makePoForm({ title, vesselId, accountId, intent: "draft" });
|
const form = makePoForm({ title, vesselId, accountId, intent: "draft" });
|
||||||
const result = await createPo(form);
|
const result = await createPo(form);
|
||||||
return (result as { id: string }).id;
|
return (result as { id: string }).id;
|
||||||
|
|
@ -55,7 +55,7 @@ async function createDraft(title: string, asUserId = techId, asRole: Parameters<
|
||||||
describe("discard — happy path", () => {
|
describe("discard — happy path", () => {
|
||||||
it("owner (TECHNICAL) can discard their own DRAFT", async () => {
|
it("owner (TECHNICAL) can discard their own DRAFT", async () => {
|
||||||
const poId = await createDraft(`${PREFIX}OwnerDiscard`);
|
const poId = await createDraft(`${PREFIX}OwnerDiscard`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
|
|
||||||
const result = await discardDraftPo(poId);
|
const result = await discardDraftPo(poId);
|
||||||
expect(result).toEqual({ ok: true });
|
expect(result).toEqual({ ok: true });
|
||||||
|
|
@ -64,7 +64,7 @@ describe("discard — happy path", () => {
|
||||||
|
|
||||||
it("MANAGER can discard any DRAFT PO (not their own)", async () => {
|
it("MANAGER can discard any DRAFT PO (not their own)", async () => {
|
||||||
const poId = await createDraft(`${PREFIX}MgrDiscard`);
|
const poId = await createDraft(`${PREFIX}MgrDiscard`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
|
|
||||||
const result = await discardDraftPo(poId);
|
const result = await discardDraftPo(poId);
|
||||||
expect(result).toEqual({ ok: true });
|
expect(result).toEqual({ ok: true });
|
||||||
|
|
@ -74,7 +74,7 @@ describe("discard — happy path", () => {
|
||||||
it("SUPERUSER can discard any DRAFT PO", async () => {
|
it("SUPERUSER can discard any DRAFT PO", async () => {
|
||||||
const superuser = await getSeedUser("admin@pelagia.local");
|
const superuser = await getSeedUser("admin@pelagia.local");
|
||||||
const poId = await createDraft(`${PREFIX}SuperDiscard`);
|
const poId = await createDraft(`${PREFIX}SuperDiscard`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(superuser.id, "SUPERUSER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(superuser.id, "SUPERUSER"));
|
||||||
|
|
||||||
const result = await discardDraftPo(poId);
|
const result = await discardDraftPo(poId);
|
||||||
expect(result).toEqual({ ok: true });
|
expect(result).toEqual({ ok: true });
|
||||||
|
|
@ -87,7 +87,7 @@ describe("discard — happy path", () => {
|
||||||
const before = await db.pOAction.findMany({ where: { poId } });
|
const before = await db.pOAction.findMany({ where: { poId } });
|
||||||
expect(before.length).toBeGreaterThan(0);
|
expect(before.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
await discardDraftPo(poId);
|
await discardDraftPo(poId);
|
||||||
|
|
||||||
const after = await db.pOAction.findMany({ where: { poId } });
|
const after = await db.pOAction.findMany({ where: { poId } });
|
||||||
|
|
@ -99,7 +99,7 @@ describe("discard — happy path", () => {
|
||||||
const linesBefore = await db.pOLineItem.findMany({ where: { poId } });
|
const linesBefore = await db.pOLineItem.findMany({ where: { poId } });
|
||||||
expect(linesBefore.length).toBeGreaterThan(0);
|
expect(linesBefore.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
await discardDraftPo(poId);
|
await discardDraftPo(poId);
|
||||||
|
|
||||||
const linesAfter = await db.pOLineItem.findMany({ where: { poId } });
|
const linesAfter = await db.pOLineItem.findMany({ where: { poId } });
|
||||||
|
|
@ -112,7 +112,7 @@ describe("discard — happy path", () => {
|
||||||
describe("discard — negative / permission tests", () => {
|
describe("discard — negative / permission tests", () => {
|
||||||
it("returns error for unauthenticated request", async () => {
|
it("returns error for unauthenticated request", async () => {
|
||||||
const poId = await createDraft(`${PREFIX}Unauth`);
|
const poId = await createDraft(`${PREFIX}Unauth`);
|
||||||
vi.mocked(auth).mockResolvedValue(null);
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(null);
|
||||||
expect(await discardDraftPo(poId)).toHaveProperty("error");
|
expect(await discardDraftPo(poId)).toHaveProperty("error");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -120,7 +120,7 @@ describe("discard — negative / permission tests", () => {
|
||||||
// Create PO as manager, try to discard as tech
|
// Create PO as manager, try to discard as tech
|
||||||
const poId = await createDraft(`${PREFIX}WrongOwner`, managerId, "MANAGER");
|
const poId = await createDraft(`${PREFIX}WrongOwner`, managerId, "MANAGER");
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
const result = await discardDraftPo(poId);
|
const result = await discardDraftPo(poId);
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
// PO must still exist
|
// PO must still exist
|
||||||
|
|
@ -129,14 +129,14 @@ describe("discard — negative / permission tests", () => {
|
||||||
|
|
||||||
it("ACCOUNTS cannot discard any PO (not in allowed roles)", async () => {
|
it("ACCOUNTS cannot discard any PO (not in allowed roles)", async () => {
|
||||||
const poId = await createDraft(`${PREFIX}AccountsForbidden`);
|
const poId = await createDraft(`${PREFIX}AccountsForbidden`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
||||||
const result = await discardDraftPo(poId);
|
const result = await discardDraftPo(poId);
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
expect(await db.purchaseOrder.findUnique({ where: { id: poId } })).not.toBeNull();
|
expect(await db.purchaseOrder.findUnique({ where: { id: poId } })).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns error for non-existent PO", async () => {
|
it("returns error for non-existent PO", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
const result = await discardDraftPo("non-existent-id");
|
const result = await discardDraftPo("non-existent-id");
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
});
|
});
|
||||||
|
|
@ -146,11 +146,11 @@ describe("discard — negative / permission tests", () => {
|
||||||
|
|
||||||
describe("discard — status guard", () => {
|
describe("discard — status guard", () => {
|
||||||
it("cannot discard a submitted (MGR_REVIEW) PO", async () => {
|
it("cannot discard a submitted (MGR_REVIEW) PO", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
const form = makePoForm({ title: `${PREFIX}Submitted`, vesselId, accountId, intent: "submit" });
|
const form = makePoForm({ title: `${PREFIX}Submitted`, vesselId, accountId, intent: "submit" });
|
||||||
const { id: poId } = (await createPo(form)) as { id: string };
|
const { id: poId } = (await createPo(form)) as { id: string };
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
const result = await discardDraftPo(poId);
|
const result = await discardDraftPo(poId);
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
const po = await db.purchaseOrder.findUnique({ where: { id: poId } });
|
const po = await db.purchaseOrder.findUnique({ where: { id: poId } });
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* Tests authorization guards and end-to-end parsing of the Sample_PO.xlsx
|
* Tests authorization guards and end-to-end parsing of the Sample_PO.xlsx
|
||||||
* fixture using the real route handler.
|
* fixture using the real route handler.
|
||||||
*/
|
*/
|
||||||
import { vi, describe, it, expect, beforeAll } from "vitest";
|
import { vi, describe, it, expect, beforeAll, beforeEach } from "vitest";
|
||||||
|
|
||||||
vi.mock("@/auth", () => ({ auth: vi.fn() }));
|
vi.mock("@/auth", () => ({ auth: vi.fn() }));
|
||||||
|
|
||||||
|
|
@ -50,13 +50,13 @@ function makeFileRequest(filePath?: string) {
|
||||||
|
|
||||||
describe("POST /api/po/import — authorization", () => {
|
describe("POST /api/po/import — authorization", () => {
|
||||||
it("returns 401 for unauthenticated requests", async () => {
|
it("returns 401 for unauthenticated requests", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(null);
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(null);
|
||||||
const res = await POST(makeFileRequest(SAMPLE_XLSX));
|
const res = await POST(makeFileRequest(SAMPLE_XLSX));
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 403 for TECHNICAL role", async () => {
|
it("returns 403 for TECHNICAL role", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
const res = await POST(makeFileRequest(SAMPLE_XLSX));
|
const res = await POST(makeFileRequest(SAMPLE_XLSX));
|
||||||
expect(res.status).toBe(403);
|
expect(res.status).toBe(403);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
@ -64,13 +64,13 @@ describe("POST /api/po/import — authorization", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 403 for ACCOUNTS role", async () => {
|
it("returns 403 for ACCOUNTS role", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
||||||
const res = await POST(makeFileRequest(SAMPLE_XLSX));
|
const res = await POST(makeFileRequest(SAMPLE_XLSX));
|
||||||
expect(res.status).toBe(403);
|
expect(res.status).toBe(403);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 200 for MANAGER role with valid file", async () => {
|
it("returns 200 for MANAGER role with valid file", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
const res = await POST(makeFileRequest(SAMPLE_XLSX));
|
const res = await POST(makeFileRequest(SAMPLE_XLSX));
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
@ -80,7 +80,7 @@ describe("POST /api/po/import — authorization", () => {
|
||||||
|
|
||||||
describe("POST /api/po/import — input validation", () => {
|
describe("POST /api/po/import — input validation", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 400 when no file is provided", async () => {
|
it("returns 400 when no file is provided", async () => {
|
||||||
|
|
@ -106,7 +106,7 @@ describe("POST /api/po/import — parsing Sample_PO.xlsx", () => {
|
||||||
let results: ParsedImport[];
|
let results: ParsedImport[];
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
const res = await POST(makeFileRequest(SAMPLE_XLSX));
|
const res = await POST(makeFileRequest(SAMPLE_XLSX));
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
results = data.results;
|
results = data.results;
|
||||||
|
|
@ -120,9 +120,9 @@ describe("POST /api/po/import — parsing Sample_PO.xlsx", () => {
|
||||||
const items = results[0].lineItems;
|
const items = results[0].lineItems;
|
||||||
const hasTcText = items.some(
|
const hasTcText = items.some(
|
||||||
(li) =>
|
(li) =>
|
||||||
li.description.toLowerCase().includes("please quote") ||
|
li.name.toLowerCase().includes("please quote") ||
|
||||||
li.description.toLowerCase().includes("delivery :") ||
|
li.name.toLowerCase().includes("delivery :") ||
|
||||||
li.description.toLowerCase().includes("payment terms")
|
li.name.toLowerCase().includes("payment terms")
|
||||||
);
|
);
|
||||||
expect(hasTcText).toBe(false);
|
expect(hasTcText).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
@ -132,7 +132,7 @@ describe("POST /api/po/import — parsing Sample_PO.xlsx", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("line item has correct description", () => {
|
it("line item has correct description", () => {
|
||||||
expect(results[0].lineItems[0].description).toBe("Eni EP 80W90 GEAR OIL");
|
expect(results[0].lineItems[0].name).toBe("Eni EP 80W90 GEAR OIL");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("line item has correct quantity (1050)", () => {
|
it("line item has correct quantity (1050)", () => {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ vi.mock("@/lib/notifier", () => ({ notify: vi.fn() }));
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { createPo } from "@/app/(portal)/po/new/actions";
|
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 { discardDraftPo } from "@/app/(portal)/po/[id]/actions";
|
import { discardDraftPo } from "@/app/(portal)/po/[id]/actions";
|
||||||
import {
|
import {
|
||||||
makeSession, getSeedUser, getSeedVessel, getSeedAccount, getSeedVendor,
|
makeSession, getSeedUser, getSeedVessel, getSeedAccount, getSeedVendor,
|
||||||
|
|
@ -48,7 +48,7 @@ afterEach(async () => {
|
||||||
|
|
||||||
describe("MANAGER — create PO", () => {
|
describe("MANAGER — create PO", () => {
|
||||||
it("MANAGER can save a PO as DRAFT", async () => {
|
it("MANAGER can save a PO as DRAFT", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
const form = makePoForm({ title: `${PREFIX}Draft`, vesselId, accountId, intent: "draft" });
|
const form = makePoForm({ title: `${PREFIX}Draft`, vesselId, accountId, intent: "draft" });
|
||||||
|
|
||||||
const result = await createPo(form);
|
const result = await createPo(form);
|
||||||
|
|
@ -59,7 +59,7 @@ describe("MANAGER — create PO", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("MANAGER can submit a PO directly", async () => {
|
it("MANAGER can submit a PO directly", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
const form = makePoForm({ title: `${PREFIX}Submit`, vesselId, accountId, intent: "submit" });
|
const form = makePoForm({ title: `${PREFIX}Submit`, vesselId, accountId, intent: "submit" });
|
||||||
|
|
||||||
const result = await createPo(form);
|
const result = await createPo(form);
|
||||||
|
|
@ -70,7 +70,7 @@ describe("MANAGER — create PO", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("MANAGER can discard their own DRAFT", async () => {
|
it("MANAGER can discard their own DRAFT", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
const form = makePoForm({ title: `${PREFIX}Discard`, vesselId, accountId, intent: "draft" });
|
const form = makePoForm({ title: `${PREFIX}Discard`, vesselId, accountId, intent: "draft" });
|
||||||
const { id: poId } = (await createPo(form)) as { id: string };
|
const { id: poId } = (await createPo(form)) as { id: string };
|
||||||
|
|
||||||
|
|
@ -80,7 +80,7 @@ describe("MANAGER — create PO", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stores correct submitterId on MANAGER-created PO", async () => {
|
it("stores correct submitterId on MANAGER-created PO", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
const form = makePoForm({ title: `${PREFIX}SubmitterId`, vesselId, accountId });
|
const form = makePoForm({ title: `${PREFIX}SubmitterId`, vesselId, accountId });
|
||||||
const { id: poId } = (await createPo(form)) as { id: string };
|
const { id: poId } = (await createPo(form)) as { id: string };
|
||||||
const po = await db.purchaseOrder.findUnique({ where: { id: poId } });
|
const po = await db.purchaseOrder.findUnique({ where: { id: poId } });
|
||||||
|
|
@ -92,14 +92,14 @@ describe("MANAGER — create PO", () => {
|
||||||
|
|
||||||
describe("role — negative permission tests for PO creation", () => {
|
describe("role — negative permission tests for PO creation", () => {
|
||||||
it("ACCOUNTS cannot create a PO", async () => {
|
it("ACCOUNTS cannot create a PO", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
||||||
const form = makePoForm({ title: `${PREFIX}AcctsForbidden`, vesselId, accountId });
|
const form = makePoForm({ title: `${PREFIX}AcctsForbidden`, vesselId, accountId });
|
||||||
const result = await createPo(form);
|
const result = await createPo(form);
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("unauthenticated request returns Unauthorized", async () => {
|
it("unauthenticated request returns Unauthorized", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(null);
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(null);
|
||||||
const form = makePoForm({ title: `${PREFIX}Unauth`, vesselId, accountId });
|
const form = makePoForm({ title: `${PREFIX}Unauth`, vesselId, accountId });
|
||||||
const result = await createPo(form);
|
const result = await createPo(form);
|
||||||
expect(result).toEqual({ error: "Unauthorized" });
|
expect(result).toEqual({ error: "Unauthorized" });
|
||||||
|
|
@ -107,7 +107,7 @@ describe("role — negative permission tests for PO creation", () => {
|
||||||
|
|
||||||
it("MANAGER cannot approve their own submitted PO (same user)", async () => {
|
it("MANAGER cannot approve their own submitted PO (same user)", async () => {
|
||||||
// Manager creates and submits
|
// Manager creates and submits
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
const form = makePoForm({
|
const form = makePoForm({
|
||||||
title: `${PREFIX}SelfApprove`,
|
title: `${PREFIX}SelfApprove`,
|
||||||
vesselId,
|
vesselId,
|
||||||
|
|
@ -120,7 +120,7 @@ describe("role — negative permission tests for PO creation", () => {
|
||||||
// Approving as the same manager — the action itself doesn't block same-user approval
|
// Approving as the same manager — the action itself doesn't block same-user approval
|
||||||
// because approval authority is role-based, not submitter-based.
|
// because approval authority is role-based, not submitter-based.
|
||||||
// This test documents the current behaviour.
|
// This test documents the current behaviour.
|
||||||
const result = await approvepo({ poId });
|
const result = await approvePo({ poId });
|
||||||
// Should succeed because MANAGER has approve_po permission and the PO has a vendor
|
// Should succeed because MANAGER has approve_po permission and the PO has a vendor
|
||||||
expect(result).toEqual({ ok: true });
|
expect(result).toEqual({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,11 @@ afterEach(async () => {
|
||||||
|
|
||||||
// Helper: create PO → submit → approve (reaches MGR_APPROVED)
|
// Helper: create PO → submit → approve (reaches MGR_APPROVED)
|
||||||
async function createApprovedPo(title: string): Promise<string> {
|
async function createApprovedPo(title: string): Promise<string> {
|
||||||
vi.mocked(auth).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 };
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
await approvePo({ poId });
|
await approvePo({ poId });
|
||||||
return poId;
|
return poId;
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +67,7 @@ describe("A-01 — approved PO appears in payment queue", () => {
|
||||||
|
|
||||||
it("processPayment transitions MGR_APPROVED to SENT_FOR_PAYMENT", async () => {
|
it("processPayment transitions MGR_APPROVED to SENT_FOR_PAYMENT", async () => {
|
||||||
const poId = await createApprovedPo(`${PREFIX}ProcessPayment`);
|
const poId = await createApprovedPo(`${PREFIX}ProcessPayment`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
||||||
|
|
||||||
const result = await processPayment({ poId });
|
const result = await processPayment({ poId });
|
||||||
expect(result).toEqual({ ok: true });
|
expect(result).toEqual({ ok: true });
|
||||||
|
|
@ -78,7 +78,7 @@ describe("A-01 — approved PO appears in payment queue", () => {
|
||||||
|
|
||||||
it("TECHNICAL role cannot process payment", async () => {
|
it("TECHNICAL role cannot process payment", async () => {
|
||||||
const poId = await createApprovedPo(`${PREFIX}PaymentForbidden`);
|
const poId = await createApprovedPo(`${PREFIX}PaymentForbidden`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
const result = await processPayment({ poId });
|
const result = await processPayment({ poId });
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
});
|
});
|
||||||
|
|
@ -90,7 +90,7 @@ describe("A-02 — mark PO as paid with reference number", () => {
|
||||||
it("transitions SENT_FOR_PAYMENT to PAID_DELIVERED and stores paymentRef", async () => {
|
it("transitions SENT_FOR_PAYMENT to PAID_DELIVERED and stores paymentRef", async () => {
|
||||||
const poId = await createApprovedPo(`${PREFIX}MarkPaid`);
|
const poId = await createApprovedPo(`${PREFIX}MarkPaid`);
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
||||||
await processPayment({ poId });
|
await processPayment({ poId });
|
||||||
const result = await markPaid({ poId, paymentRef: "NEFT/2026/001234", paymentDate: TODAY });
|
const result = await markPaid({ poId, paymentRef: "NEFT/2026/001234", paymentDate: TODAY });
|
||||||
expect(result).toEqual({ ok: true });
|
expect(result).toEqual({ ok: true });
|
||||||
|
|
@ -105,7 +105,7 @@ describe("A-02 — mark PO as paid with reference number", () => {
|
||||||
it("creates a PAYMENT_SENT action in the audit trail", async () => {
|
it("creates a PAYMENT_SENT action in the audit trail", async () => {
|
||||||
const poId = await createApprovedPo(`${PREFIX}PaidAudit`);
|
const poId = await createApprovedPo(`${PREFIX}PaidAudit`);
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
||||||
await processPayment({ poId });
|
await processPayment({ poId });
|
||||||
await markPaid({ poId, paymentRef: "TXN-9999", paymentDate: TODAY });
|
await markPaid({ poId, paymentRef: "TXN-9999", paymentDate: TODAY });
|
||||||
|
|
||||||
|
|
@ -117,7 +117,7 @@ describe("A-02 — mark PO as paid with reference number", () => {
|
||||||
it("returns error when paymentRef is missing", async () => {
|
it("returns error when paymentRef is missing", async () => {
|
||||||
const poId = await createApprovedPo(`${PREFIX}PaidNoRef`);
|
const poId = await createApprovedPo(`${PREFIX}PaidNoRef`);
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
||||||
await processPayment({ poId });
|
await processPayment({ poId });
|
||||||
const result = await markPaid({ poId, paymentRef: "", paymentDate: TODAY });
|
const result = await markPaid({ poId, paymentRef: "", paymentDate: TODAY });
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
|
|
@ -126,7 +126,7 @@ describe("A-02 — mark PO as paid with reference number", () => {
|
||||||
it("returns error when payment date is in the future", async () => {
|
it("returns error when payment date is in the future", async () => {
|
||||||
const poId = await createApprovedPo(`${PREFIX}PaidFutureDate`);
|
const poId = await createApprovedPo(`${PREFIX}PaidFutureDate`);
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
||||||
await processPayment({ poId });
|
await processPayment({ poId });
|
||||||
const future = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
const future = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
||||||
const result = await markPaid({ poId, paymentRef: "FUTURE-REF", paymentDate: future });
|
const result = await markPaid({ poId, paymentRef: "FUTURE-REF", paymentDate: future });
|
||||||
|
|
@ -137,7 +137,7 @@ describe("A-02 — mark PO as paid with reference number", () => {
|
||||||
const { notify } = await import("@/lib/notifier");
|
const { notify } = await import("@/lib/notifier");
|
||||||
const poId = await createApprovedPo(`${PREFIX}PaidNotify`);
|
const poId = await createApprovedPo(`${PREFIX}PaidNotify`);
|
||||||
|
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
||||||
vi.mocked(notify).mockClear();
|
vi.mocked(notify).mockClear();
|
||||||
await processPayment({ poId });
|
await processPayment({ poId });
|
||||||
await markPaid({ poId, paymentRef: "REF-42", paymentDate: TODAY });
|
await markPaid({ poId, paymentRef: "REF-42", paymentDate: TODAY });
|
||||||
|
|
@ -149,10 +149,10 @@ describe("A-02 — mark PO as paid with reference number", () => {
|
||||||
it("MANAGER role cannot mark as paid (wrong permission)", async () => {
|
it("MANAGER role cannot mark as paid (wrong permission)", async () => {
|
||||||
const poId = await createApprovedPo(`${PREFIX}PaidMgrForbidden`);
|
const poId = await createApprovedPo(`${PREFIX}PaidMgrForbidden`);
|
||||||
|
|
||||||
vi.mocked(auth).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).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
const result = await markPaid({ poId, paymentRef: "MGR-REF", paymentDate: TODAY });
|
const result = await markPaid({ poId, paymentRef: "MGR-REF", paymentDate: TODAY });
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* Integration tests for GET /api/products/search.
|
* Integration tests for GET /api/products/search.
|
||||||
* Tests authorization, query validation, filtering, and Decimal serialisation.
|
* Tests authorization, query validation, filtering, and Decimal serialisation.
|
||||||
*/
|
*/
|
||||||
import { vi, describe, it, expect, beforeAll } from "vitest";
|
import { vi, describe, it, expect, beforeAll, beforeEach } from "vitest";
|
||||||
|
|
||||||
vi.mock("@/auth", () => ({ auth: vi.fn() }));
|
vi.mock("@/auth", () => ({ auth: vi.fn() }));
|
||||||
|
|
||||||
|
|
@ -31,19 +31,19 @@ function makeRequest(query: string) {
|
||||||
|
|
||||||
describe("GET /api/products/search — authorization", () => {
|
describe("GET /api/products/search — authorization", () => {
|
||||||
it("returns 401 for unauthenticated requests", async () => {
|
it("returns 401 for unauthenticated requests", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(null);
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(null);
|
||||||
const res = await GET(makeRequest("oil"));
|
const res = await GET(makeRequest("oil"));
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("TECHNICAL can search products", async () => {
|
it("TECHNICAL can search products", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
const res = await GET(makeRequest("oil"));
|
const res = await GET(makeRequest("oil"));
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ACCOUNTS can search products", async () => {
|
it("ACCOUNTS can search products", async () => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
||||||
const res = await GET(makeRequest("oil"));
|
const res = await GET(makeRequest("oil"));
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
@ -53,7 +53,7 @@ describe("GET /api/products/search — authorization", () => {
|
||||||
|
|
||||||
describe("GET /api/products/search — query validation", () => {
|
describe("GET /api/products/search — query validation", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns empty array for query shorter than 2 chars", async () => {
|
it("returns empty array for query shorter than 2 chars", async () => {
|
||||||
|
|
@ -79,7 +79,7 @@ describe("GET /api/products/search — query validation", () => {
|
||||||
|
|
||||||
describe("GET /api/products/search — search behaviour", () => {
|
describe("GET /api/products/search — search behaviour", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("finds products by name substring", async () => {
|
it("finds products by name substring", async () => {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ vi.mock("@/lib/notifier", () => ({ notify: vi.fn() }));
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { createPo } from "@/app/(portal)/po/new/actions";
|
import { createPo } from "@/app/(portal)/po/new/actions";
|
||||||
import { approvepo, requestVendorId } from "@/app/(portal)/approvals/[id]/actions";
|
import { approvePo, requestVendorId } from "@/app/(portal)/approvals/[id]/actions";
|
||||||
import { provideVendorId } from "@/app/(portal)/po/[id]/actions";
|
import { provideVendorId } from "@/app/(portal)/po/[id]/actions";
|
||||||
import {
|
import {
|
||||||
makeSession, getSeedUser, getSeedVessel, getSeedAccount, getSeedVendor,
|
makeSession, getSeedUser, getSeedVessel, getSeedAccount, getSeedVendor,
|
||||||
|
|
@ -76,7 +76,7 @@ afterEach(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function makeReviewPo(title: string, withVendor = false) {
|
async function makeReviewPo(title: string, withVendor = false) {
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
|
||||||
const form = makePoForm({
|
const form = makePoForm({
|
||||||
title,
|
title,
|
||||||
vesselId,
|
vesselId,
|
||||||
|
|
@ -93,9 +93,9 @@ async function makeReviewPo(title: string, withVendor = false) {
|
||||||
describe("approval — vendor required", () => {
|
describe("approval — vendor required", () => {
|
||||||
it("blocks approval when PO has no vendor assigned", async () => {
|
it("blocks approval when PO has no vendor assigned", async () => {
|
||||||
const poId = await makeReviewPo(`${PREFIX}NoVendorBlock`);
|
const poId = await makeReviewPo(`${PREFIX}NoVendorBlock`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
|
|
||||||
const result = await approvepo({ poId });
|
const result = await approvePo({ poId });
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
expect((result as { error: string }).error).toMatch(/vendor/i);
|
expect((result as { error: string }).error).toMatch(/vendor/i);
|
||||||
|
|
||||||
|
|
@ -105,9 +105,9 @@ describe("approval — vendor required", () => {
|
||||||
|
|
||||||
it("allows approval when PO has a vendor assigned", async () => {
|
it("allows approval when PO has a vendor assigned", async () => {
|
||||||
const poId = await makeReviewPo(`${PREFIX}VendorPresent`, true);
|
const poId = await makeReviewPo(`${PREFIX}VendorPresent`, true);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
|
|
||||||
const result = await approvepo({ poId });
|
const result = await approvePo({ poId });
|
||||||
expect(result).toEqual({ ok: true });
|
expect(result).toEqual({ ok: true });
|
||||||
|
|
||||||
const po = await db.purchaseOrder.findUnique({ where: { id: poId } });
|
const po = await db.purchaseOrder.findUnique({ where: { id: poId } });
|
||||||
|
|
@ -120,14 +120,14 @@ describe("approval — vendor required", () => {
|
||||||
describe("provideVendorId — role expansion", () => {
|
describe("provideVendorId — role expansion", () => {
|
||||||
async function makePendingPo(title: string) {
|
async function makePendingPo(title: string) {
|
||||||
const poId = await makeReviewPo(title);
|
const poId = await makeReviewPo(title);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
|
||||||
await requestVendorId({ poId });
|
await requestVendorId({ poId });
|
||||||
return poId;
|
return poId;
|
||||||
}
|
}
|
||||||
|
|
||||||
it("ACCOUNTS can provide a verified vendor ID", async () => {
|
it("ACCOUNTS can provide a verified vendor ID", async () => {
|
||||||
const poId = await makePendingPo(`${PREFIX}AccountsProvide`);
|
const poId = await makePendingPo(`${PREFIX}AccountsProvide`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
||||||
|
|
||||||
const result = await provideVendorId({ poId, vendorId: verifiedVendorId });
|
const result = await provideVendorId({ poId, vendorId: verifiedVendorId });
|
||||||
expect(result).toEqual({ ok: true });
|
expect(result).toEqual({ ok: true });
|
||||||
|
|
@ -139,7 +139,7 @@ describe("provideVendorId — role expansion", () => {
|
||||||
|
|
||||||
it("rejects an unverified vendor (no vendorId field on Vendor record)", async () => {
|
it("rejects an unverified vendor (no vendorId field on Vendor record)", async () => {
|
||||||
const poId = await makePendingPo(`${PREFIX}UnverifiedVendor`);
|
const poId = await makePendingPo(`${PREFIX}UnverifiedVendor`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
||||||
|
|
||||||
const result = await provideVendorId({ poId, vendorId: unverifiedVendorDbId });
|
const result = await provideVendorId({ poId, vendorId: unverifiedVendorDbId });
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
|
|
@ -150,7 +150,7 @@ describe("provideVendorId — role expansion", () => {
|
||||||
|
|
||||||
it("AUDITOR cannot provide vendor ID", async () => {
|
it("AUDITOR cannot provide vendor ID", async () => {
|
||||||
const poId = await makePendingPo(`${PREFIX}AuditorDenied`);
|
const poId = await makePendingPo(`${PREFIX}AuditorDenied`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(auditorId, "AUDITOR"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(auditorId, "AUDITOR"));
|
||||||
|
|
||||||
const result = await provideVendorId({ poId, vendorId: verifiedVendorId });
|
const result = await provideVendorId({ poId, vendorId: verifiedVendorId });
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
|
|
@ -159,7 +159,7 @@ describe("provideVendorId — role expansion", () => {
|
||||||
it("returns error when called on a PO not in VENDOR_ID_PENDING state", async () => {
|
it("returns error when called on a PO not in VENDOR_ID_PENDING state", async () => {
|
||||||
// PO still in MGR_REVIEW — no requestVendorId called
|
// PO still in MGR_REVIEW — no requestVendorId called
|
||||||
const poId = await makeReviewPo(`${PREFIX}WrongState`);
|
const poId = await makeReviewPo(`${PREFIX}WrongState`);
|
||||||
vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
|
||||||
|
|
||||||
const result = await provideVendorId({ poId, vendorId: verifiedVendorId });
|
const result = await provideVendorId({ poId, vendorId: verifiedVendorId });
|
||||||
expect(result).toHaveProperty("error");
|
expect(result).toHaveProperty("error");
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,10 @@ describe("Permissions", () => {
|
||||||
expect(hasPermission("MANAGER", "approve_po")).toBe(true);
|
expect(hasPermission("MANAGER", "approve_po")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("MANAGER cannot process payment", () => {
|
// MANAGER was intentionally granted process_payment in commit e1340b9
|
||||||
expect(hasPermission("MANAGER", "process_payment")).toBe(false);
|
// ("chore(perm): manager permissions fix 2").
|
||||||
|
it("MANAGER can process payment", () => {
|
||||||
|
expect(hasPermission("MANAGER", "process_payment")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ACCOUNTS can process payment", () => {
|
it("ACCOUNTS can process payment", () => {
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,17 @@
|
||||||
* Tests parseSheet() against the real Sample_PO.xlsx fixture and synthetic
|
* Tests parseSheet() against the real Sample_PO.xlsx fixture and synthetic
|
||||||
* workbooks built in-memory, without any HTTP or database layer.
|
* workbooks built in-memory, without any HTTP or database layer.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect, beforeAll } from "vitest";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync, existsSync } from "fs";
|
||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
import * as XLSX from "xlsx";
|
import * as XLSX from "xlsx";
|
||||||
import { parseSheet, parseWorkbook, cellStr, cellNum } from "@/lib/po-import-parser";
|
import { parseSheet, parseWorkbook, cellStr, cellNum } from "@/lib/po-import-parser";
|
||||||
|
|
||||||
const SAMPLE_PATH = resolve(__dirname, "../../../../Prototype/Sample_PO.xlsx");
|
const SAMPLE_PATH = resolve(__dirname, "../../../../Prototype/Sample_PO.xlsx");
|
||||||
|
// The original Sample_PO.xlsx lives outside the repo, so these fixture-backed
|
||||||
|
// tests skip wherever the file is absent (CI, other machines). The synthetic
|
||||||
|
// workbook tests below exercise the parser everywhere.
|
||||||
|
const HAS_SAMPLE = existsSync(SAMPLE_PATH);
|
||||||
|
|
||||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -77,7 +81,7 @@ describe("cellNum", () => {
|
||||||
|
|
||||||
// ── parseSheet against real Sample_PO.xlsx ───────────────────────────────────
|
// ── parseSheet against real Sample_PO.xlsx ───────────────────────────────────
|
||||||
|
|
||||||
describe("parseSheet — Sample_PO.xlsx", () => {
|
describe.skipIf(!HAS_SAMPLE)("parseSheet — Sample_PO.xlsx", () => {
|
||||||
let parsed: ReturnType<typeof parseSheet>;
|
let parsed: ReturnType<typeof parseSheet>;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
|
@ -248,7 +252,7 @@ describe("parseSheet — synthetic edge cases", () => {
|
||||||
// ── parseWorkbook ─────────────────────────────────────────────────────────────
|
// ── parseWorkbook ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe("parseWorkbook", () => {
|
describe("parseWorkbook", () => {
|
||||||
it("parses the real Sample_PO.xlsx and returns one result", () => {
|
it.skipIf(!HAS_SAMPLE)("parses the real Sample_PO.xlsx and returns one result", () => {
|
||||||
const buffer = readFileSync(SAMPLE_PATH);
|
const buffer = readFileSync(SAMPLE_PATH);
|
||||||
const results = parseWorkbook(buffer);
|
const results = parseWorkbook(buffer);
|
||||||
expect(results).toHaveLength(1);
|
expect(results).toHaveLength(1);
|
||||||
|
|
|
||||||
|
|
@ -49,14 +49,16 @@ Each PR must include:
|
||||||
**Enforcement** — [`.forgejo/workflows/pr-checks.yml`](../.forgejo/workflows/pr-checks.yml)
|
**Enforcement** — [`.forgejo/workflows/pr-checks.yml`](../.forgejo/workflows/pr-checks.yml)
|
||||||
runs on every PR into `master`:
|
runs on every PR into `master`:
|
||||||
|
|
||||||
1. **Test-presence gate (hard):** a PR touching `App/app|lib|components|hooks` with no
|
1. **Test-presence gate:** a PR touching `App/app|lib|components|hooks` with no test
|
||||||
test change fails. Justify genuine exceptions in the PR body for a reviewer to override.
|
change fails. Justify genuine exceptions in the PR body for a reviewer to override.
|
||||||
2. **App-code type-check (hard):** no new `tsc` errors in application code. (The test
|
2. **Type-check:** `pnpm type-check` must be clean across the whole project (tests
|
||||||
suite has a known pre-existing type-mismatch baseline, tracked separately, so it is
|
included). The test suite's old type baseline was repaired when this gate landed.
|
||||||
filtered out of this gate.)
|
3. **Unit tests:** `pnpm test` must pass.
|
||||||
3. **Unit tests (advisory):** reported but non-blocking until the unit baseline is green
|
|
||||||
(2 known failures on `master`). `pnpm lint` is intentionally not run — it currently
|
All three are **hard** gates. `pnpm lint` is intentionally not run — it currently
|
||||||
requires an interactive ESLint migration.
|
requires an interactive ESLint migration (a follow-up). Integration tests are
|
||||||
|
type-checked here but executed against the `pelagia_test` DB by the autofix / locally
|
||||||
|
(not in this shared CI, to avoid prod-mirror schema drift).
|
||||||
|
|
||||||
A [`PULL_REQUEST_TEMPLATE.md`](../.forgejo/PULL_REQUEST_TEMPLATE.md) carries the checklist.
|
A [`PULL_REQUEST_TEMPLATE.md`](../.forgejo/PULL_REQUEST_TEMPLATE.md) carries the checklist.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue