mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
chore: test fixes
This commit is contained in:
@@ -61,6 +61,71 @@ services:
|
|||||||
- mc alias set e2e http://minio-e2e:9000 e2e-minio e2e-minio-secret && mc mb --ignore-existing e2e/fhc
|
- mc alias set e2e http://minio-e2e:9000 e2e-minio e2e-minio-secret && mc mb --ignore-existing e2e/fhc
|
||||||
restart: "no"
|
restart: "no"
|
||||||
|
|
||||||
|
# Stand-in for eSignet's token/userinfo endpoints (see fayda-mock/server.js).
|
||||||
|
# The real Fayda authorization step (phone + SMS OTP) can't run in e2e —
|
||||||
|
# Cypress bypasses the popup and drives POST start / POST complete directly,
|
||||||
|
# so this only needs to answer the token exchange + userinfo calls that
|
||||||
|
# `completeVerification` makes server-side.
|
||||||
|
fayda-mock-e2e:
|
||||||
|
image: node:20-alpine
|
||||||
|
volumes:
|
||||||
|
- ./e2e/freight/fayda-mock:/app:ro
|
||||||
|
working_dir: /app
|
||||||
|
command: ["node", "server.js"]
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"fetch('http://localhost:4400/userinfo').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
|
||||||
|
]
|
||||||
|
interval: 3s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
# Stand-in for https://etrade.gov.et — ETradeService's base URL is
|
||||||
|
# hardcoded (not env-configurable like Fayda's endpoints), so this is
|
||||||
|
# reached by DNS alias instead: the "etrade.gov.et" network alias below
|
||||||
|
# makes freight-api-e2e's real hardcoded requests land here. See
|
||||||
|
# etrade-mock/server.js.
|
||||||
|
etrade-mock-e2e:
|
||||||
|
# Debian-based (not -alpine): ships openssl in the base image, so the
|
||||||
|
# self-signed cert below needs no package install at container start
|
||||||
|
# (alpine's apk would have to fetch it from the network every boot).
|
||||||
|
image: node:20
|
||||||
|
volumes:
|
||||||
|
- ./e2e/freight/etrade-mock:/app:ro
|
||||||
|
working_dir: /app
|
||||||
|
environment:
|
||||||
|
TLS_CERT_PATH: /tmp/etrade-mock/cert.pem
|
||||||
|
TLS_KEY_PATH: /tmp/etrade-mock/key.pem
|
||||||
|
# Throwaway self-signed cert generated fresh into /tmp on every
|
||||||
|
# container start — nothing shaped like a key/cert is committed (the
|
||||||
|
# code mount above is :ro, so this couldn't write there anyway).
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- >
|
||||||
|
mkdir -p /tmp/etrade-mock &&
|
||||||
|
openssl req -x509 -newkey rsa:2048 -keyout "$$TLS_KEY_PATH" -out "$$TLS_CERT_PATH" -days 1 -nodes -subj "/CN=etrade.gov.et" 2>/dev/null &&
|
||||||
|
node server.js
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- etrade.gov.et
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"require('https').get({hostname:'localhost',port:443,path:'/api/BusinessMain/GetBusinessByLicenseNo?LicenseNo=x&Tin=x&Lang=en',rejectUnauthorized:false},r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))",
|
||||||
|
]
|
||||||
|
interval: 3s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
freight-api-e2e:
|
freight-api-e2e:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
@@ -74,6 +139,10 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
minio-init-e2e:
|
minio-init-e2e:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
fayda-mock-e2e:
|
||||||
|
condition: service_healthy
|
||||||
|
etrade-mock-e2e:
|
||||||
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
PORT: "3001"
|
PORT: "3001"
|
||||||
DB_HOST: postgres-freight-e2e
|
DB_HOST: postgres-freight-e2e
|
||||||
@@ -104,7 +173,21 @@ services:
|
|||||||
MINIO_REGION: us-east-1
|
MINIO_REGION: us-east-1
|
||||||
# External integrations off
|
# External integrations off
|
||||||
RABBITMQ_ENABLED: "false"
|
RABBITMQ_ENABLED: "false"
|
||||||
FAYDA_ENABLED: "false"
|
# Fayda ON, pointed at the local mock (fayda-mock-e2e) instead of the
|
||||||
|
# real eSignet infra — see that service's comment above. Cypress drives
|
||||||
|
# verification via the API (start + complete), never the real popup.
|
||||||
|
FAYDA_ENABLED: "true"
|
||||||
|
FAYDA_CLIENT_ID: e2e-fayda-client
|
||||||
|
FAYDA_AUTHORIZATION_ENDPOINT: http://fayda-mock-e2e:4400/authorize
|
||||||
|
FAYDA_TOKEN_ENDPOINT: http://fayda-mock-e2e:4400/token
|
||||||
|
FAYDA_USERINFO_ENDPOINT: http://fayda-mock-e2e:4400/userinfo
|
||||||
|
FAYDA_REDIRECT_URI: http://localhost:${E2E_PORTAL_PORT:-5373}/callback
|
||||||
|
FAYDA_PORTAL_REDIRECT_URI: http://localhost:${E2E_PORTAL_PORT:-5373}/callback
|
||||||
|
# Throwaway e2e-only RSA JWK (client_assertion signing) — the mock
|
||||||
|
# never verifies the signature, this just has to be well-formed.
|
||||||
|
# Generated fresh per launch by e2e.mjs (fakeFaydaPrivateKeyBase64),
|
||||||
|
# not committed here, so nothing shaped like a private key sits in git.
|
||||||
|
FAYDA_PRIVATE_KEY_BASE64: ${FAYDA_PRIVATE_KEY_BASE64}
|
||||||
# SMS strategy has no kill switch and defaults to a real dev endpoint —
|
# SMS strategy has no kill switch and defaults to a real dev endpoint —
|
||||||
# blackhole it so e2e never sends SMS (failures are logged, non-fatal).
|
# blackhole it so e2e never sends SMS (failures are logged, non-fatal).
|
||||||
OZIKING_SMS_URL: http://127.0.0.1:9/sms
|
OZIKING_SMS_URL: http://127.0.0.1:9/sms
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
acceptExport,
|
acceptExport,
|
||||||
apiPost,
|
apiPost,
|
||||||
bookBulk,
|
bookBulk,
|
||||||
|
clearToOperationRequestPending,
|
||||||
createImportSchedule,
|
createImportSchedule,
|
||||||
db,
|
db,
|
||||||
departureAt,
|
departureAt,
|
||||||
@@ -83,8 +84,9 @@ describe("booking cancel: pre-commit only, blocked once allocated", { retries: 0
|
|||||||
|
|
||||||
it("an un-accepted booking cancels, its invoice is expired, and re-cancel is rejected", () => {
|
it("an un-accepted booking cancels, its invoice is expired, and re-cancel is rejected", () => {
|
||||||
bookBulk({ suffix: "CXA", tons: 700, scheduledDate: BOOKING_DAY });
|
bookBulk({ suffix: "CXA", tons: 700, scheduledDate: BOOKING_DAY });
|
||||||
|
clearToOperationRequestPending("CXA", BOOKING_DAY);
|
||||||
|
|
||||||
// Fresh non-customs export booking sits at OPERATION_REQUEST_PENDING.
|
// Cleared non-customs export booking sits at OPERATION_REQUEST_PENDING.
|
||||||
withBooking("CXA", (b) => {
|
withBooking("CXA", (b) => {
|
||||||
expect(b.status, "pre-accept status").to.eq("OPERATION_REQUEST_PENDING");
|
expect(b.status, "pre-accept status").to.eq("OPERATION_REQUEST_PENDING");
|
||||||
cancel(b.id).then((res) => expect(res.status, "cancelled").to.be.oneOf([200, 201]));
|
cancel(b.id).then((res) => expect(res.status, "cancelled").to.be.oneOf([200, 201]));
|
||||||
@@ -112,6 +114,7 @@ describe("booking cancel: pre-commit only, blocked once allocated", { retries: 0
|
|||||||
|
|
||||||
it("a PAID + allocated booking cannot be cancelled — the commit gate blocks it", () => {
|
it("a PAID + allocated booking cannot be cancelled — the commit gate blocks it", () => {
|
||||||
bookBulk({ suffix: "CXB", tons: 700, scheduledDate: BOOKING_DAY });
|
bookBulk({ suffix: "CXB", tons: 700, scheduledDate: BOOKING_DAY });
|
||||||
|
clearToOperationRequestPending("CXB", BOOKING_DAY);
|
||||||
acceptExport("CXB");
|
acceptExport("CXB");
|
||||||
markPaid("CXB");
|
markPaid("CXB");
|
||||||
pollAllocations("CXB", 10); // 700T / 70T = 10 CW4 wagons
|
pollAllocations("CXB", 10); // 700T / 70T = 10 CW4 wagons
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import {
|
|||||||
acceptOperation,
|
acceptOperation,
|
||||||
apiPost,
|
apiPost,
|
||||||
bookBulk,
|
bookBulk,
|
||||||
|
clearIntercityToFullyExecuted,
|
||||||
|
clearToOperationRequestPending,
|
||||||
closeBookingWindow,
|
closeBookingWindow,
|
||||||
completeDocReview,
|
completeDocReview,
|
||||||
createImportSchedule,
|
createImportSchedule,
|
||||||
@@ -98,8 +100,10 @@ describe("bulk critical matrix: gates, sub-corridor, bulk intercity, whole-train
|
|||||||
|
|
||||||
it("a through-corridor 1 400 T booking and a NAGAD→MOJO 700 T booking share the train", () => {
|
it("a through-corridor 1 400 T booking and a NAGAD→MOJO 700 T booking share the train", () => {
|
||||||
bookBulk({ suffix: "BM1", tons: 1400, scheduledDate: BOOKING_DAY });
|
bookBulk({ suffix: "BM1", tons: 1400, scheduledDate: BOOKING_DAY });
|
||||||
|
clearToOperationRequestPending("BM1", BOOKING_DAY);
|
||||||
acceptOperation("BM1");
|
acceptOperation("BM1");
|
||||||
bookBulk({ suffix: "BMSUB", tons: 700, scheduledDate: BOOKING_DAY });
|
bookBulk({ suffix: "BMSUB", tons: 700, scheduledDate: BOOKING_DAY });
|
||||||
|
clearToOperationRequestPending("BMSUB", BOOKING_DAY);
|
||||||
acceptOperation("BMSUB");
|
acceptOperation("BMSUB");
|
||||||
|
|
||||||
withSchedule(DEPARTURE, (s) => {
|
withSchedule(DEPARTURE, (s) => {
|
||||||
@@ -122,7 +126,9 @@ describe("bulk critical matrix: gates, sub-corridor, bulk intercity, whole-train
|
|||||||
|
|
||||||
it("bulk intercity ride-along: dateless DOMESTIC wheat accepted onto the import train's free leg", () => {
|
it("bulk intercity ride-along: dateless DOMESTIC wheat accepted onto the import train's free leg", () => {
|
||||||
bookBulk({ suffix: "BMIC", tons: 140 }); // 2 wagons MOJO → KALITY, dateless
|
bookBulk({ suffix: "BMIC", tons: 140 }); // 2 wagons MOJO → KALITY, dateless
|
||||||
acceptOperation("BMIC");
|
// DOMESTIC bookings skip the shipment-day step entirely — finalize alone
|
||||||
|
// lands FULLY_EXECUTED; staff assign it to a passing train below.
|
||||||
|
clearIntercityToFullyExecuted("BMIC");
|
||||||
|
|
||||||
withSchedule(DEPARTURE, (s) => {
|
withSchedule(DEPARTURE, (s) => {
|
||||||
withBooking("BMIC", (b) => {
|
withBooking("BMIC", (b) => {
|
||||||
@@ -159,6 +165,7 @@ describe("bulk critical matrix: gates, sub-corridor, bulk intercity, whole-train
|
|||||||
withSchedule(GIANT_DEPARTURE, (s) => forceWindowOpen(s.id, 45));
|
withSchedule(GIANT_DEPARTURE, (s) => forceWindowOpen(s.id, 45));
|
||||||
|
|
||||||
bookBulk({ suffix: "BMG", tons: 4000, scheduledDate: GIANT_DAY });
|
bookBulk({ suffix: "BMG", tons: 4000, scheduledDate: GIANT_DAY });
|
||||||
|
clearToOperationRequestPending("BMG", GIANT_DAY);
|
||||||
acceptOperation("BMG");
|
acceptOperation("BMG");
|
||||||
withSchedule(GIANT_DEPARTURE, (s) => {
|
withSchedule(GIANT_DEPARTURE, (s) => {
|
||||||
closeBookingWindow(s.id);
|
closeBookingWindow(s.id);
|
||||||
@@ -208,7 +215,7 @@ describe("bulk critical matrix: gates, sub-corridor, bulk intercity, whole-train
|
|||||||
expectFailure: "must take the whole",
|
expectFailure: "must take the whole",
|
||||||
});
|
});
|
||||||
bookBulk({ suffix: "BMG", tons: 220, scheduledDate: REMAINDER_DAY });
|
bookBulk({ suffix: "BMG", tons: 220, scheduledDate: REMAINDER_DAY });
|
||||||
pollBookingStatus("BMG", "OPERATION_REQUEST_PENDING", 5);
|
clearToOperationRequestPending("BMG", REMAINDER_DAY);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
acceptExport,
|
acceptExport,
|
||||||
apiPost,
|
apiPost,
|
||||||
bookBulk,
|
bookBulk,
|
||||||
|
clearToOperationRequestPending,
|
||||||
completeBookingMilestone,
|
completeBookingMilestone,
|
||||||
createImportSchedule,
|
createImportSchedule,
|
||||||
db,
|
db,
|
||||||
@@ -103,7 +104,7 @@ describe("bulk export: six wheat bookings fill the 54-wagon CW4 train (FCFS)", {
|
|||||||
it("six exporters book wheat inside the one window", () => {
|
it("six exporters book wheat inside the one window", () => {
|
||||||
BOOKINGS.forEach((b) => {
|
BOOKINGS.forEach((b) => {
|
||||||
bookBulk({ suffix: b.suffix, tons: b.tons, scheduledDate: BOOKING_DAY });
|
bookBulk({ suffix: b.suffix, tons: b.tons, scheduledDate: BOOKING_DAY });
|
||||||
pollBookingStatus(b.suffix, "OPERATION_REQUEST_PENDING", 5);
|
clearToOperationRequestPending(b.suffix, BOOKING_DAY);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
acceptOperation,
|
acceptOperation,
|
||||||
apiPost,
|
apiPost,
|
||||||
bookBulk,
|
bookBulk,
|
||||||
|
clearToOperationRequestPending,
|
||||||
closeBookingWindow,
|
closeBookingWindow,
|
||||||
completeBookingMilestone,
|
completeBookingMilestone,
|
||||||
completeDocReview,
|
completeDocReview,
|
||||||
@@ -103,7 +104,7 @@ describe("bulk import: six wheat bookings fill the 54-wagon CW4 train", { retrie
|
|||||||
it("customer books all six wheat shipments inside the first window", () => {
|
it("customer books all six wheat shipments inside the first window", () => {
|
||||||
BOOKINGS.forEach((b) => {
|
BOOKINGS.forEach((b) => {
|
||||||
bookBulk({ suffix: b.suffix, tons: b.tons, scheduledDate: BOOKING_DAY });
|
bookBulk({ suffix: b.suffix, tons: b.tons, scheduledDate: BOOKING_DAY });
|
||||||
pollBookingStatus(b.suffix, "OPERATION_REQUEST_PENDING", 5);
|
clearToOperationRequestPending(b.suffix, BOOKING_DAY);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
import {
|
import {
|
||||||
acceptOperation,
|
acceptOperation,
|
||||||
bookBulk,
|
bookBulk,
|
||||||
|
clearToOperationRequestPending,
|
||||||
closeBookingWindow,
|
closeBookingWindow,
|
||||||
completeDocReview,
|
completeDocReview,
|
||||||
createImportSchedule,
|
createImportSchedule,
|
||||||
@@ -84,6 +85,7 @@ describe("bulk import: split offer, remainder rebooking, expiry + promotion", {
|
|||||||
it("seven customers book wheat in the first window; operations accepts them in priority order", () => {
|
it("seven customers book wheat in the first window; operations accepts them in priority order", () => {
|
||||||
ORDER.forEach((suffix) => {
|
ORDER.forEach((suffix) => {
|
||||||
bookBulk({ suffix, tons: TONS[suffix], scheduledDate: BOOKING_DAY });
|
bookBulk({ suffix, tons: TONS[suffix], scheduledDate: BOOKING_DAY });
|
||||||
|
clearToOperationRequestPending(suffix, BOOKING_DAY);
|
||||||
acceptOperation(suffix);
|
acceptOperation(suffix);
|
||||||
});
|
});
|
||||||
ORDER.forEach((suffix, i) => setPriority(suffix, i + 1));
|
ORDER.forEach((suffix, i) => setPriority(suffix, i + 1));
|
||||||
@@ -192,7 +194,7 @@ describe("bulk import: split offer, remainder rebooking, expiry + promotion", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
bookBulk({ suffix: "BSC", tons: 1120, scheduledDate: REMAINDER_DAY });
|
bookBulk({ suffix: "BSC", tons: 1120, scheduledDate: REMAINDER_DAY });
|
||||||
pollBookingStatus("BSC", "OPERATION_REQUEST_PENDING", 5);
|
clearToOperationRequestPending("BSC", REMAINDER_DAY);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ describe("contract lifecycle: creation to finalization", { retries: 0 }, () => {
|
|||||||
cy.mantineSelect(/^Contract Kind/, "General Contract");
|
cy.mantineSelect(/^Contract Kind/, "General Contract");
|
||||||
cy.mantineSelect(/^New or Renewal/, "New Contract");
|
cy.mantineSelect(/^New or Renewal/, "New Contract");
|
||||||
cy.contains("Rail Transport Only", { timeout: 15000 }).click();
|
cy.contains("Rail Transport Only", { timeout: 15000 }).click();
|
||||||
cy.mantineSelect(/^Payment Currency/, /^ETB/);
|
// Contracts are always quoted in USD now — the billing currency is picked
|
||||||
|
// per shipment at booking time, not here.
|
||||||
cy.contains("button", "Continue").click({ force: true });
|
cy.contains("button", "Continue").click({ force: true });
|
||||||
|
|
||||||
// Step 1 — Cargo & Route. Container contracts now auto-cover BOTH 20ft &
|
// Step 1 — Cargo & Route. Container contracts now auto-cover BOTH 20ft &
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
acceptExport,
|
acceptExport,
|
||||||
apiPost,
|
apiPost,
|
||||||
bookContainers,
|
bookContainers,
|
||||||
|
clearToOperationRequestPending,
|
||||||
createImportSchedule,
|
createImportSchedule,
|
||||||
db,
|
db,
|
||||||
dbBooking,
|
dbBooking,
|
||||||
@@ -118,7 +119,7 @@ describe("export: six bookings fill the 54-wagon corridor train (FCFS)", { retri
|
|||||||
scheduledDate: BOOKING_DAY,
|
scheduledDate: BOOKING_DAY,
|
||||||
});
|
});
|
||||||
isoSeed += b.twenty + b.forty;
|
isoSeed += b.twenty + b.forty;
|
||||||
pollBookingStatus(b.suffix, "OPERATION_REQUEST_PENDING", 5);
|
clearToOperationRequestPending(b.suffix, BOOKING_DAY);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
* Sequential steps of one journey — retries off (steps are not idempotent).
|
* Sequential steps of one journey — retries off (steps are not idempotent).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { clearBookingClearance } from "./import-utils";
|
||||||
|
|
||||||
const customer = "user@gmail.com";
|
const customer = "user@gmail.com";
|
||||||
const companyTin = "0102030405"; // seed-company.sql
|
const companyTin = "0102030405"; // seed-company.sql
|
||||||
const opsStaff = "operation@edr.local";
|
const opsStaff = "operation@edr.local";
|
||||||
@@ -477,7 +479,8 @@ describe("export one-time journeys: container + bulk on one train", { retries: 0
|
|||||||
cy.mantineSelect(/^Contract Kind/, "One-Time Contract");
|
cy.mantineSelect(/^Contract Kind/, "One-Time Contract");
|
||||||
cy.mantineSelect(/^New or Renewal/, "New Contract");
|
cy.mantineSelect(/^New or Renewal/, "New Contract");
|
||||||
cy.contains("button", "Rail Transport Only", { timeout: 15000 }).click({ force: true });
|
cy.contains("button", "Rail Transport Only", { timeout: 15000 }).click({ force: true });
|
||||||
cy.mantineSelect(/^Payment Currency/, /^ETB/);
|
// Contracts are always quoted in USD now — the billing currency is picked
|
||||||
|
// per shipment at booking time, not here.
|
||||||
cy.contains("button", "Continue").click({ force: true });
|
cy.contains("button", "Continue").click({ force: true });
|
||||||
|
|
||||||
// Container contracts auto-cover both 20ft & 40ft (no size picker) and the
|
// Container contracts auto-cover both 20ft & 40ft (no size picker) and the
|
||||||
@@ -537,6 +540,12 @@ describe("export one-time journeys: container + bulk on one train", { retries: 0
|
|||||||
cy.contains("button", "Confirm & book").click();
|
cy.contains("button", "Confirm & book").click();
|
||||||
cy.location("pathname", { timeout: 30000 }).should("match", /^\/bookings\/.+/);
|
cy.location("pathname", { timeout: 30000 }).should("match", /^\/bookings\/.+/);
|
||||||
|
|
||||||
|
withBooking("CONTAINER", (b) => {
|
||||||
|
expect(b.status, "born in the clearance gate").to.eq("AWAITING_DOCUMENTS");
|
||||||
|
expect(b.scheduled_date, "export bookings carry a shipment day").to.be.a("string");
|
||||||
|
clearBookingClearance(b.id, b.scheduled_date!);
|
||||||
|
});
|
||||||
|
|
||||||
withBooking("CONTAINER", (b) => {
|
withBooking("CONTAINER", (b) => {
|
||||||
expect(b.status).to.eq("OPERATION_REQUEST_PENDING");
|
expect(b.status).to.eq("OPERATION_REQUEST_PENDING");
|
||||||
expect(b.scheduled_date, "export bookings carry a shipment day").to.be.a("string");
|
expect(b.scheduled_date, "export bookings carry a shipment day").to.be.a("string");
|
||||||
@@ -561,7 +570,8 @@ describe("export one-time journeys: container + bulk on one train", { retries: 0
|
|||||||
cy.mantineSelect(/^Contract Kind/, "One-Time Contract");
|
cy.mantineSelect(/^Contract Kind/, "One-Time Contract");
|
||||||
cy.mantineSelect(/^New or Renewal/, "New Contract");
|
cy.mantineSelect(/^New or Renewal/, "New Contract");
|
||||||
cy.contains("button", "Rail Transport Only", { timeout: 15000 }).click({ force: true });
|
cy.contains("button", "Rail Transport Only", { timeout: 15000 }).click({ force: true });
|
||||||
cy.mantineSelect(/^Payment Currency/, /^ETB/);
|
// Contracts are always quoted in USD now — the billing currency is picked
|
||||||
|
// per shipment at booking time, not here.
|
||||||
cy.contains("button", "Continue").click({ force: true });
|
cy.contains("button", "Continue").click({ force: true });
|
||||||
|
|
||||||
cy.mantineSelect(/^Cargo Scope/, /General \/ Bulk cargo/);
|
cy.mantineSelect(/^Cargo Scope/, /General \/ Bulk cargo/);
|
||||||
@@ -605,6 +615,11 @@ describe("export one-time journeys: container + bulk on one train", { retries: 0
|
|||||||
cy.contains("button", "Confirm & book").click();
|
cy.contains("button", "Confirm & book").click();
|
||||||
cy.location("pathname", { timeout: 30000 }).should("match", /^\/bookings\/.+/);
|
cy.location("pathname", { timeout: 30000 }).should("match", /^\/bookings\/.+/);
|
||||||
|
|
||||||
|
withBooking("BULK", (b) => {
|
||||||
|
expect(b.status, "born in the clearance gate").to.eq("AWAITING_DOCUMENTS");
|
||||||
|
clearBookingClearance(b.id, b.scheduled_date!);
|
||||||
|
});
|
||||||
|
|
||||||
withBooking("BULK", (b) => {
|
withBooking("BULK", (b) => {
|
||||||
expect(b.status).to.eq("OPERATION_REQUEST_PENDING");
|
expect(b.status).to.eq("OPERATION_REQUEST_PENDING");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -69,6 +69,44 @@ export function apiPost(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drive Fayda identity verification via the API, bypassing the real
|
||||||
|
* popup+SMS flow entirely — `fayda-mock-e2e` stands in for eSignet's
|
||||||
|
* token/userinfo endpoints (see docker-compose.e2e.yaml), so a throwaway
|
||||||
|
* code is enough: start a session, then complete it — mirrors what
|
||||||
|
* FaydaVerifyPanel's popup + postMessage dance does server-side, minus the
|
||||||
|
* UI. Uses the browser's OWN auth-token cookie rather than `tokenFor` —
|
||||||
|
* this only ever runs for a user already logged in in the running test
|
||||||
|
* (the real flow requires being on an authenticated page to see the
|
||||||
|
* "Verify with Fayda" button at all), and `tokenFor` only knows how to log
|
||||||
|
* in fixed seeded accounts, not a signup created earlier in the same test
|
||||||
|
* with its own freshly-chosen password.
|
||||||
|
*/
|
||||||
|
export function completeFaydaVerification(subject: "owner" | "poa") {
|
||||||
|
return cy.getCookie("auth-token").then((cookie) => {
|
||||||
|
expect(cookie, "authenticated session (auth-token cookie)").to.not.be.null;
|
||||||
|
const headers = { Authorization: `Bearer ${cookie!.value}` };
|
||||||
|
return cy
|
||||||
|
.request({
|
||||||
|
method: "POST",
|
||||||
|
url: `${apiUrl()}/api/fayda/verification/start`,
|
||||||
|
headers,
|
||||||
|
body: { purpose: "VERIFY", platform: "PORTAL" },
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
const authorizationUrl: string = res.body.data.authorizationUrl;
|
||||||
|
const state = new URL(authorizationUrl).searchParams.get("state");
|
||||||
|
expect(state, "fayda session state").to.be.a("string");
|
||||||
|
return cy.request({
|
||||||
|
method: "POST",
|
||||||
|
url: `${apiUrl()}/api/companies/identity/fayda/complete`,
|
||||||
|
headers,
|
||||||
|
body: { subject, code: "e2e-mock-code", state },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function apiGet(email: string, path: string, failOnStatusCode = true) {
|
export function apiGet(email: string, path: string, failOnStatusCode = true) {
|
||||||
return tokenFor(email).then((token) =>
|
return tokenFor(email).then((token) =>
|
||||||
cy.request({
|
cy.request({
|
||||||
@@ -457,13 +495,86 @@ export function bookBulk(opts: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload one ad-hoc doc → GL approves it → finalize → customer proceeds with
|
||||||
|
* the shipment day. The e2e seed configures no required documents, so one
|
||||||
|
* doc satisfies the 100%-approved gate. Takes a bookingId directly so specs
|
||||||
|
* with their own booking lookup (not the *-suffix convention) can reuse it.
|
||||||
|
*
|
||||||
|
* KNOWN GAP — customs (`customsClearingEnabled`) bookings do NOT use this
|
||||||
|
* path: `finalizeClearance` rejects them outright ("General customs bookings
|
||||||
|
* use phased clearance — complete milestones via the phased actions instead
|
||||||
|
* of finalize"). Their real flow is a separate multi-step phased chain
|
||||||
|
* (transit permit upload → finalizePreClearance → delivery order upload for
|
||||||
|
* IMPORT; a shorter EXPORT_RELEASED milestone for EXPORT) that no spec here
|
||||||
|
* drives yet — see booking-clearance.service.ts / clearance-workflow.service.ts.
|
||||||
|
* Calling this on a customs booking 400s at the finalize step; callers with
|
||||||
|
* customs suffixes in the mix (the full_train specs' BF-series customs:
|
||||||
|
* true bookings) currently accept that those specific bookings — and only
|
||||||
|
* those — fail here until the phased-clearance walk is written.
|
||||||
|
*/
|
||||||
|
export function clearBookingClearance(bookingId: string, scheduledDate: string) {
|
||||||
|
glUpload(`/api/bookings/${bookingId}/clearance/documents`, {}, "custom_e2e");
|
||||||
|
apiPost(superAdmin, `/api/bookings/${bookingId}/clearance/review`, {
|
||||||
|
fileKey: "custom_e2e",
|
||||||
|
status: "APPROVED",
|
||||||
|
})
|
||||||
|
.its("status")
|
||||||
|
.should("be.oneOf", [200, 201]);
|
||||||
|
apiPost(superAdmin, `/api/bookings/${bookingId}/clearance/finalize`)
|
||||||
|
.its("status")
|
||||||
|
.should("be.oneOf", [200, 201]);
|
||||||
|
apiPost(customer, `/api/bookings/${bookingId}/clearance/proceed`, { scheduledDate })
|
||||||
|
.its("status")
|
||||||
|
.should("be.oneOf", [200, 201]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every contract booking is now born in the clearance gate — walk a *-suffix
|
||||||
|
* booking from AWAITING_DOCUMENTS to OPERATION_REQUEST_PENDING via
|
||||||
|
* {@link clearBookingClearance}.
|
||||||
|
*/
|
||||||
|
export function clearToOperationRequestPending(suffix: string, scheduledDate: string) {
|
||||||
|
withBooking(suffix, (b) => {
|
||||||
|
expect(b.status, `${suffix} starts in the clearance gate`).to.eq("AWAITING_DOCUMENTS");
|
||||||
|
clearBookingClearance(b.id, scheduledDate);
|
||||||
|
});
|
||||||
|
pollBookingStatus(suffix, "OPERATION_REQUEST_PENDING", 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DOMESTIC (intercity) bookings never pick a shipment day — there is no
|
||||||
|
* `clearance/proceed` step for them. `finalizeClearance` sends an approved
|
||||||
|
* intercity booking straight to FULLY_EXECUTED (the ride-along pool); staff
|
||||||
|
* assign it to a passing train separately. Takes a bookingId directly, like
|
||||||
|
* {@link clearBookingClearance}.
|
||||||
|
*/
|
||||||
|
export function clearIntercityBookingClearance(bookingId: string) {
|
||||||
|
glUpload(`/api/bookings/${bookingId}/clearance/documents`, {}, "custom_e2e");
|
||||||
|
apiPost(superAdmin, `/api/bookings/${bookingId}/clearance/review`, {
|
||||||
|
fileKey: "custom_e2e",
|
||||||
|
status: "APPROVED",
|
||||||
|
})
|
||||||
|
.its("status")
|
||||||
|
.should("be.oneOf", [200, 201]);
|
||||||
|
apiPost(superAdmin, `/api/bookings/${bookingId}/clearance/finalize`)
|
||||||
|
.its("status")
|
||||||
|
.should("be.oneOf", [200, 201]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Walk a *-suffix DOMESTIC booking from AWAITING_DOCUMENTS to FULLY_EXECUTED. */
|
||||||
|
export function clearIntercityToFullyExecuted(suffix: string) {
|
||||||
|
withBooking(suffix, (b) => {
|
||||||
|
expect(b.status, `${suffix} starts in the clearance gate`).to.eq("AWAITING_DOCUMENTS");
|
||||||
|
clearIntercityBookingClearance(b.id);
|
||||||
|
});
|
||||||
|
pollBookingStatus(suffix, "FULLY_EXECUTED", 10);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Walk a GENERAL booking through its PER-BOOKING clearance chain (Path A):
|
* Walk a GENERAL booking through its PER-BOOKING clearance chain (Path A):
|
||||||
* AWAITING_DOCUMENTS → upload one doc → GL approves it → finalize →
|
* AWAITING_DOCUMENTS → ... → OPERATION_REQUEST_PENDING (clearToOperationRequestPending)
|
||||||
* CLEARANCE_READY → customer proceeds with the shipment day →
|
* → ops accept → FULLY_EXECUTED (pool).
|
||||||
* OPERATION_REQUEST_PENDING → ops accept → FULLY_EXECUTED (pool). The e2e
|
|
||||||
* seed configures no required documents, so one ad-hoc doc satisfies the
|
|
||||||
* 100%-approved gate.
|
|
||||||
*/
|
*/
|
||||||
export function clearGeneralBooking(
|
export function clearGeneralBooking(
|
||||||
suffix: string,
|
suffix: string,
|
||||||
@@ -471,22 +582,7 @@ export function clearGeneralBooking(
|
|||||||
/** EXPORT ends at acceptExport (FCFS reserves on accept), not the pool. */
|
/** EXPORT ends at acceptExport (FCFS reserves on accept), not the pool. */
|
||||||
mode: "import" | "export" = "import",
|
mode: "import" | "export" = "import",
|
||||||
) {
|
) {
|
||||||
withBooking(suffix, (b) => {
|
clearToOperationRequestPending(suffix, scheduledDate);
|
||||||
expect(b.status, `${suffix} starts in the clearance gate`).to.eq("AWAITING_DOCUMENTS");
|
|
||||||
glUpload(`/api/bookings/${b.id}/clearance/documents`, {}, "custom_e2e");
|
|
||||||
apiPost(superAdmin, `/api/bookings/${b.id}/clearance/review`, {
|
|
||||||
fileKey: "custom_e2e",
|
|
||||||
status: "APPROVED",
|
|
||||||
})
|
|
||||||
.its("status")
|
|
||||||
.should("be.oneOf", [200, 201]);
|
|
||||||
apiPost(superAdmin, `/api/bookings/${b.id}/clearance/finalize`)
|
|
||||||
.its("status")
|
|
||||||
.should("be.oneOf", [200, 201]);
|
|
||||||
apiPost(customer, `/api/bookings/${b.id}/clearance/proceed`, { scheduledDate })
|
|
||||||
.its("status")
|
|
||||||
.should("be.oneOf", [200, 201]);
|
|
||||||
});
|
|
||||||
if (mode === "export") acceptExport(suffix);
|
if (mode === "export") acceptExport(suffix);
|
||||||
else acceptOperation(suffix);
|
else acceptOperation(suffix);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
acceptOperation,
|
acceptOperation,
|
||||||
apiPost,
|
apiPost,
|
||||||
bookContainers,
|
bookContainers,
|
||||||
|
clearToOperationRequestPending,
|
||||||
closeBookingWindow,
|
closeBookingWindow,
|
||||||
completeBookingMilestone,
|
completeBookingMilestone,
|
||||||
completeDocReview,
|
completeDocReview,
|
||||||
@@ -113,7 +114,7 @@ describe("import: six bookings fill the 54-wagon corridor train", { retries: 0 }
|
|||||||
scheduledDate: BOOKING_DAY,
|
scheduledDate: BOOKING_DAY,
|
||||||
});
|
});
|
||||||
isoSeed += b.twenty + b.forty;
|
isoSeed += b.twenty + b.forty;
|
||||||
pollBookingStatus(b.suffix, "OPERATION_REQUEST_PENDING", 5);
|
clearToOperationRequestPending(b.suffix, BOOKING_DAY);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
resetCorridorDay,
|
resetCorridorDay,
|
||||||
acceptOperation,
|
acceptOperation,
|
||||||
bookContainers,
|
bookContainers,
|
||||||
|
clearToOperationRequestPending,
|
||||||
closeBookingWindow,
|
closeBookingWindow,
|
||||||
completeDocReview,
|
completeDocReview,
|
||||||
createImportSchedule,
|
createImportSchedule,
|
||||||
@@ -91,6 +92,7 @@ describe("import: split offer, remainder rebooking, expiry + promotion", { retri
|
|||||||
scheduledDate: BOOKING_DAY,
|
scheduledDate: BOOKING_DAY,
|
||||||
});
|
});
|
||||||
isoSeed += s.twenty + s.forty;
|
isoSeed += s.twenty + s.forty;
|
||||||
|
clearToOperationRequestPending(suffix, BOOKING_DAY);
|
||||||
acceptOperation(suffix);
|
acceptOperation(suffix);
|
||||||
});
|
});
|
||||||
ORDER.forEach((suffix, i) => setPriority(suffix, i + 1));
|
ORDER.forEach((suffix, i) => setPriority(suffix, i + 1));
|
||||||
@@ -217,7 +219,7 @@ describe("import: split offer, remainder rebooking, expiry + promotion", { retri
|
|||||||
twenty: 32,
|
twenty: 32,
|
||||||
scheduledDate: REMAINDER_DAY,
|
scheduledDate: REMAINDER_DAY,
|
||||||
});
|
});
|
||||||
pollBookingStatus("SC", "OPERATION_REQUEST_PENDING", 5);
|
clearToOperationRequestPending("SC", REMAINDER_DAY);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,11 @@
|
|||||||
* 7. backoffice — operations approves the document, then finalizes document
|
* 7. backoffice — operations approves the document, then finalizes document
|
||||||
* approval → FULLY_EXECUTED
|
* approval → FULLY_EXECUTED
|
||||||
* 8. portal — customer books 2 × 20ft under the contract (intercity has
|
* 8. portal — customer books 2 × 20ft under the contract (intercity has
|
||||||
* no shipment date) → booking OPERATION_REQUEST_PENDING
|
* no shipment date) → booking AWAITING_DOCUMENTS → customer
|
||||||
* 9. backoffice — operations accepts the operation request → booking
|
* uploads a doc, GL approves, finalize → DOMESTIC bookings
|
||||||
* FULLY_EXECUTED (intercity waiting pool)
|
* skip the shipment-day step entirely, so finalize alone
|
||||||
|
* lands FULLY_EXECUTED (the intercity waiting pool) — there
|
||||||
|
* is no separate operations "accept the request" step.
|
||||||
* 10. backoffice — operations creates the EXPORT route
|
* 10. backoffice — operations creates the EXPORT route
|
||||||
* Mojo → Dire Dawa → Djibouti Port (distances seeded)
|
* Mojo → Dire Dawa → Djibouti Port (distances seeded)
|
||||||
* 11. backoffice — operations schedules the export train (built Train-Builder
|
* 11. backoffice — operations schedules the export train (built Train-Builder
|
||||||
@@ -34,6 +36,8 @@
|
|||||||
* Sequential steps of one journey — retries off (steps are not idempotent).
|
* Sequential steps of one journey — retries off (steps are not idempotent).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { clearIntercityBookingClearance } from "./import-utils";
|
||||||
|
|
||||||
const customer = "user@gmail.com";
|
const customer = "user@gmail.com";
|
||||||
const companyTin = "0102030405"; // seed-company.sql
|
const companyTin = "0102030405"; // seed-company.sql
|
||||||
const opsStaff = "operation@edr.local";
|
const opsStaff = "operation@edr.local";
|
||||||
@@ -153,12 +157,13 @@ function createIntercityContract() {
|
|||||||
cy.loginPortal(customer);
|
cy.loginPortal(customer);
|
||||||
cy.visitPortal("/contracts/new");
|
cy.visitPortal("/contracts/new");
|
||||||
|
|
||||||
// Step 0 — Setup. Intercity forces ETB and hides the customs section.
|
// Step 0 — Setup. Intercity hides the customs section; the billing currency
|
||||||
|
// step moved to booking time (intercity always bills ETB there, per app
|
||||||
|
// comment in step2-service-type.tsx), so it is not selected here.
|
||||||
cy.mantineSelect(/^Operation Type/, /^Intercity$/);
|
cy.mantineSelect(/^Operation Type/, /^Intercity$/);
|
||||||
cy.mantineSelect(/^Contract Kind/, "One-Time Contract");
|
cy.mantineSelect(/^Contract Kind/, "One-Time Contract");
|
||||||
cy.mantineSelect(/^New or Renewal/, "New Contract");
|
cy.mantineSelect(/^New or Renewal/, "New Contract");
|
||||||
cy.contains("button", "Rail Transport Only", { timeout: 15000 }).click();
|
cy.contains("button", "Rail Transport Only", { timeout: 15000 }).click();
|
||||||
cy.mantineSelect(/^Payment Currency/, /^ETB/);
|
|
||||||
cy.contains("button", "Continue").click({ force: true });
|
cy.contains("button", "Continue").click({ force: true });
|
||||||
|
|
||||||
// Step 1 — Cargo & Route (Ethiopian yards only for intercity). Container
|
// Step 1 — Cargo & Route (Ethiopian yards only for intercity). Container
|
||||||
@@ -422,21 +427,15 @@ describe("intercity one-time journey: contract → booking → export train", {
|
|||||||
cy.location("pathname", { timeout: 30000 }).should("match", /^\/bookings\/.+/);
|
cy.location("pathname", { timeout: 30000 }).should("match", /^\/bookings\/.+/);
|
||||||
|
|
||||||
withBooking((b) => {
|
withBooking((b) => {
|
||||||
expect(b.status).to.eq("OPERATION_REQUEST_PENDING");
|
expect(b.status, "born in the clearance gate").to.eq("AWAITING_DOCUMENTS");
|
||||||
expect(b.scheduled_date, "intercity bookings carry no scheduled date").to.eq(null);
|
clearIntercityBookingClearance(b.id);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it("operations accepts the operation request — booking joins the intercity pool", () => {
|
|
||||||
cy.loginBackoffice(opsStaff);
|
|
||||||
withBooking((b) => cy.visit(`/dashboard/booking-requests/${b.id}`));
|
|
||||||
|
|
||||||
cy.contains("button", /Accept operation|^Accept$/, { timeout: 20000 }).click();
|
|
||||||
cy.contains("Accept operation request?", { timeout: 15000 }).should("be.visible");
|
|
||||||
cy.get(".mantine-Modal-content").contains("button", /^Accept$/).click();
|
|
||||||
|
|
||||||
withBooking((b) => {
|
withBooking((b) => {
|
||||||
expect(b.status, "accepted intercity booking waits in the pool").to.eq("FULLY_EXECUTED");
|
expect(b.status, "DOMESTIC finalize skips straight to the pool").to.eq(
|
||||||
|
"FULLY_EXECUTED",
|
||||||
|
);
|
||||||
|
expect(b.scheduled_date, "intercity bookings carry no scheduled date").to.eq(null);
|
||||||
expect(b.train_schedule_id).to.eq(null);
|
expect(b.train_schedule_id).to.eq(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
apiPost,
|
apiPost,
|
||||||
bookBulk,
|
bookBulk,
|
||||||
bookContainers,
|
bookContainers,
|
||||||
|
clearToOperationRequestPending,
|
||||||
completeBookingMilestone,
|
completeBookingMilestone,
|
||||||
createImportSchedule,
|
createImportSchedule,
|
||||||
db,
|
db,
|
||||||
@@ -120,7 +121,7 @@ describe("mixed export: containers and wheat share one 54-wagon FCFS train", { r
|
|||||||
} else {
|
} else {
|
||||||
bookBulk({ suffix: b.suffix, tons: b.tons!, scheduledDate: BOOKING_DAY });
|
bookBulk({ suffix: b.suffix, tons: b.tons!, scheduledDate: BOOKING_DAY });
|
||||||
}
|
}
|
||||||
pollBookingStatus(b.suffix, "OPERATION_REQUEST_PENDING", 5);
|
clearToOperationRequestPending(b.suffix, BOOKING_DAY);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
apiPost,
|
apiPost,
|
||||||
bookBulk,
|
bookBulk,
|
||||||
bookContainers,
|
bookContainers,
|
||||||
|
clearToOperationRequestPending,
|
||||||
closeBookingWindow,
|
closeBookingWindow,
|
||||||
completeBookingMilestone,
|
completeBookingMilestone,
|
||||||
completeDocReview,
|
completeDocReview,
|
||||||
@@ -113,7 +114,7 @@ describe("mixed import: containers and wheat share one 54-wagon train", { retrie
|
|||||||
} else {
|
} else {
|
||||||
bookBulk({ suffix: b.suffix, tons: b.tons!, scheduledDate: BOOKING_DAY });
|
bookBulk({ suffix: b.suffix, tons: b.tons!, scheduledDate: BOOKING_DAY });
|
||||||
}
|
}
|
||||||
pollBookingStatus(b.suffix, "OPERATION_REQUEST_PENDING", 5);
|
clearToOperationRequestPending(b.suffix, BOOKING_DAY);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
acceptOperation,
|
acceptOperation,
|
||||||
bookBulk,
|
bookBulk,
|
||||||
bookContainers,
|
bookContainers,
|
||||||
|
clearToOperationRequestPending,
|
||||||
closeBookingWindow,
|
closeBookingWindow,
|
||||||
completeDocReview,
|
completeDocReview,
|
||||||
createImportSchedule,
|
createImportSchedule,
|
||||||
@@ -96,10 +97,12 @@ describe("mixed import: container split; container-freed wagons promote the bulk
|
|||||||
scheduledDate: BOOKING_DAY,
|
scheduledDate: BOOKING_DAY,
|
||||||
});
|
});
|
||||||
isoSeed += b.twenty;
|
isoSeed += b.twenty;
|
||||||
|
clearToOperationRequestPending(b.suffix, BOOKING_DAY);
|
||||||
acceptOperation(b.suffix);
|
acceptOperation(b.suffix);
|
||||||
});
|
});
|
||||||
BULKS.forEach((b) => {
|
BULKS.forEach((b) => {
|
||||||
bookBulk({ suffix: b.suffix, tons: b.tons, scheduledDate: BOOKING_DAY });
|
bookBulk({ suffix: b.suffix, tons: b.tons, scheduledDate: BOOKING_DAY });
|
||||||
|
clearToOperationRequestPending(b.suffix, BOOKING_DAY);
|
||||||
acceptOperation(b.suffix);
|
acceptOperation(b.suffix);
|
||||||
});
|
});
|
||||||
CONTAINERS.forEach((b, i) => setPriority(b.suffix, i + 1));
|
CONTAINERS.forEach((b, i) => setPriority(b.suffix, i + 1));
|
||||||
@@ -209,7 +212,7 @@ describe("mixed import: container split; container-freed wagons promote the bulk
|
|||||||
twenty: 20,
|
twenty: 20,
|
||||||
scheduledDate: REMAINDER_DAY,
|
scheduledDate: REMAINDER_DAY,
|
||||||
});
|
});
|
||||||
pollBookingStatus("MSX", "OPERATION_REQUEST_PENDING", 5);
|
clearToOperationRequestPending("MSX", REMAINDER_DAY);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,15 +18,15 @@
|
|||||||
* journey's user/company from the DB instead of module variables.
|
* journey's user/company from the DB instead of module variables.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { completeFaydaVerification } from "./import-utils";
|
||||||
|
|
||||||
const stamp = Date.now();
|
const stamp = Date.now();
|
||||||
const email = `e2e.onboard.${stamp}@example.com`;
|
const email = `e2e.onboard.${stamp}@example.com`;
|
||||||
// Ethiopian mobile: 9 + 8 digits, unique per run.
|
// Ethiopian mobile: 9 + 8 digits, unique per run.
|
||||||
const phoneNational = `9${String(stamp).slice(-8)}`;
|
const phoneNational = `9${String(stamp).slice(-8)}`;
|
||||||
const signupPassword = "Password@e2e1";
|
const signupPassword = "Password@e2e1";
|
||||||
const companyName = `E2E Onboard Co ${stamp}`;
|
|
||||||
const tin = String(stamp).slice(-10).padStart(10, "1");
|
const tin = String(stamp).slice(-10).padStart(10, "1");
|
||||||
const vat = String(stamp + 1).slice(-10).padStart(10, "2");
|
const vat = String(stamp + 1).slice(-10).padStart(10, "2");
|
||||||
const fan = String(stamp).slice(-13).padStart(16, "3");
|
|
||||||
|
|
||||||
const portal = () => Cypress.env("portalUrl") as string;
|
const portal = () => Cypress.env("portalUrl") as string;
|
||||||
|
|
||||||
@@ -61,12 +61,6 @@ function fillPhone(index: number, national: string) {
|
|||||||
|
|
||||||
describe("customer onboarding journey", { retries: 0 }, () => {
|
describe("customer onboarding journey", { retries: 0 }, () => {
|
||||||
it("signs up with OTP and completes the onboarding wizard", () => {
|
it("signs up with OTP and completes the onboarding wizard", () => {
|
||||||
// The eTrade TIN lookup 400s in e2e (external service unreachable). The
|
|
||||||
// form handles it ("fill in the details manually") but axios also throws
|
|
||||||
// an uncaught rejection — ignore just that one.
|
|
||||||
cy.on("uncaught:exception", (err) =>
|
|
||||||
err.message.includes("Request failed with status code 400") ? false : true,
|
|
||||||
);
|
|
||||||
cy.visit(`${portal()}/signup`);
|
cy.visit(`${portal()}/signup`);
|
||||||
|
|
||||||
fill(/^First name/, "Onboard");
|
fill(/^First name/, "Onboard");
|
||||||
@@ -91,20 +85,39 @@ describe("customer onboarding journey", { retries: 0 }, () => {
|
|||||||
cy.contains("button", "Importer").click();
|
cy.contains("button", "Importer").click();
|
||||||
cy.get(".mantine-Modal-content").contains("button", "Continue").click();
|
cy.get(".mantine-Modal-content").contains("button", "Continue").click();
|
||||||
|
|
||||||
// Company step. TIN first — the eTrade auto-lookup fails in e2e (no
|
// Ethiopian companies gate the "Owner identity" step on Fayda
|
||||||
// external network) and the form allows manual entry.
|
// verification — a real popup + SMS OTP flow that can't run in e2e.
|
||||||
cy.get('input[placeholder="0012345678"]', { timeout: 15000 }).type(tin);
|
// Complete it via the API against fayda-mock-e2e (the profile this
|
||||||
fill(/^Company Name/, companyName);
|
// attaches to was just created by the nationality/role step above).
|
||||||
fill(/^Company Email/, `ops.${stamp}@example.com`);
|
// The wizard already fetched `identity` once when this step mounted —
|
||||||
fillPhone(0, "911234567");
|
// completing verification out-of-band (no popup, so no onVerified
|
||||||
fill(/^Location/, "Addis Ababa, Ethiopia");
|
// callback fires) leaves that fetch stale, so reload to force a fresh
|
||||||
|
// one. Wizard progress resumes server-side, so this doesn't lose the
|
||||||
|
// nationality/role step just completed.
|
||||||
|
completeFaydaVerification("owner");
|
||||||
|
cy.reload();
|
||||||
|
cy.contains("Confirm your VAT number", { timeout: 20000 }).should(
|
||||||
|
"be.visible",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Company step. TIN auto-triggers the eTrade lookup once it's a full 10
|
||||||
|
// digits (mocked in e2e — see docker-compose.e2e.yaml's etrade-mock-e2e).
|
||||||
|
// A successful lookup locks Company Name/Region/Zone/Woreda/Kebele/House
|
||||||
|
// No as read-only (ETradeCompanyCard) — nothing left to type there, and
|
||||||
|
// Company Email/Phone/Location were dropped from this step entirely (the
|
||||||
|
// Fayda-verified owner supplies contact details now). By label, not
|
||||||
|
// placeholder: the VAT Number field on this same step shares the TIN
|
||||||
|
// field's "0012345678" placeholder, so a placeholder selector matches 2.
|
||||||
|
fill(/^TIN Number/, tin);
|
||||||
|
cy.contains("Verified with eTrade", { timeout: 15000 }).should(
|
||||||
|
"be.visible",
|
||||||
|
);
|
||||||
|
// handleETradeDataLoaded sets several fields in sequence (name, region,
|
||||||
|
// zone, woreda, kebele, houseNo) — each a render, still settling right
|
||||||
|
// after the badge appears. Typing into VAT immediately raced one of
|
||||||
|
// those and detached mid-type; let it finish before touching the form.
|
||||||
|
cy.wait(500);
|
||||||
fill(/^VAT Number/, vat);
|
fill(/^VAT Number/, vat);
|
||||||
cy.get('input[placeholder="1234567890123456"]').type(fan);
|
|
||||||
cy.mantineSelect(/^Region/, "Addis Ababa");
|
|
||||||
fill(/^Zone/, "Zone 1");
|
|
||||||
fill(/^Woreda/, "Woreda 1");
|
|
||||||
fill(/^Kebele/, "Kebele 1");
|
|
||||||
fill(/^House No/, "123");
|
|
||||||
cy.get(".mantine-Modal-content").contains("button", "Continue").click();
|
cy.get(".mantine-Modal-content").contains("button", "Continue").click();
|
||||||
|
|
||||||
// Personnel (general manager).
|
// Personnel (general manager).
|
||||||
|
|||||||
@@ -141,34 +141,55 @@ Cypress.Commands.add("mantineSelect", (label: string | RegExp, option: string |
|
|||||||
* takes explicit Start/End dates and pre-fills neither, so "Accept & start
|
* takes explicit Start/End dates and pre-fills neither, so "Accept & start
|
||||||
* approval" stays disabled until both are set.
|
* approval" stays disabled until both are set.
|
||||||
*
|
*
|
||||||
* Mantine's DateInput parses typed text with its valueFormat, which defaults to
|
* The fields are Mantine DateTimePickers — a button that opens a calendar
|
||||||
* "MMMM D, YYYY" — the same shape en-US toLocaleDateString produces.
|
* popover, not a typeable input. Zoom out to the decade view (2 clicks on the
|
||||||
|
* header's middle control, which has no [data-direction]) then drill back
|
||||||
|
* down year → month → day, and confirm with the popover's submit (check)
|
||||||
|
* button — clicking a day alone only stages the value, it does not close
|
||||||
|
* the popover or commit the pick.
|
||||||
*/
|
*/
|
||||||
|
const MONTH_ABBR = [
|
||||||
|
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||||
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
||||||
|
] as const;
|
||||||
|
const MONTH_NAMES = [
|
||||||
|
"January", "February", "March", "April", "May", "June",
|
||||||
|
"July", "August", "September", "October", "November", "December",
|
||||||
|
] as const;
|
||||||
|
|
||||||
Cypress.Commands.add("acceptValidityWindow", (days = 365) => {
|
Cypress.Commands.add("acceptValidityWindow", (days = 365) => {
|
||||||
const start = new Date();
|
const start = new Date();
|
||||||
const end = new Date(start.getTime() + days * 24 * 60 * 60 * 1000);
|
const end = new Date(start.getTime() + days * 24 * 60 * 60 * 1000);
|
||||||
const asInput = (d: Date) =>
|
|
||||||
d.toLocaleDateString("en-US", {
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
year: "numeric",
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const [label, value] of [
|
const pickDate = (label: string, date: Date) => {
|
||||||
["Start date", start],
|
|
||||||
["End date", end],
|
|
||||||
] as const) {
|
|
||||||
cy.contains("label", label)
|
cy.contains("label", label)
|
||||||
.invoke("attr", "for")
|
.invoke("attr", "for")
|
||||||
.then((id) => {
|
.then((id) => cy.get(`[id="${id}"]`).click({ force: true }));
|
||||||
cy.get(`[id="${id}"]`)
|
|
||||||
.clear({ force: true })
|
// month view -> year view -> decade view.
|
||||||
.type(asInput(value), { force: true })
|
for (let i = 0; i < 2; i++) {
|
||||||
// DateInput commits on blur; it also closes the calendar popover,
|
cy.get('.mantine-Popover-dropdown [data-direction="previous"]')
|
||||||
// which would otherwise sit over the submit button.
|
.siblings("button")
|
||||||
.blur();
|
.first()
|
||||||
});
|
.click({ force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cy.get(".mantine-Popover-dropdown")
|
||||||
|
.contains("button", new RegExp(`^${date.getFullYear()}$`))
|
||||||
|
.click({ force: true });
|
||||||
|
cy.get(".mantine-Popover-dropdown")
|
||||||
|
.contains("button", new RegExp(`^${MONTH_ABBR[date.getMonth()]}$`))
|
||||||
|
.click({ force: true });
|
||||||
|
|
||||||
|
const dayAriaLabel = `${date.getDate()} ${MONTH_NAMES[date.getMonth()]} ${date.getFullYear()}`;
|
||||||
|
cy.get(`.mantine-Popover-dropdown [aria-label="${dayAriaLabel}"]`).click({
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
cy.get(".mantine-DateTimePicker-submitButton").click({ force: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
pickDate("Start date", start);
|
||||||
|
pickDate("End date", end);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
124
e2e/freight/etrade-mock/server.js
Normal file
124
e2e/freight/etrade-mock/server.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
// Stand-in for https://etrade.gov.et in the e2e stack. ETradeService's base
|
||||||
|
// URL is hardcoded (apps/edr-freight-api/src/modules/companies/services/etrade.service.ts),
|
||||||
|
// not env-configurable, so this is reached by aliasing this container AS
|
||||||
|
// "etrade.gov.et" on the compose network (see docker-compose.e2e.yaml) rather
|
||||||
|
// than by pointing an env var here. HTTPS because the real service is HTTPS
|
||||||
|
// — the cert itself only needs to exist, never be trusted, since
|
||||||
|
// ETradeService's httpsAgent sets rejectUnauthorized:false. The compose
|
||||||
|
// service's command generates a throwaway self-signed cert into
|
||||||
|
// TLS_CERT_PATH/TLS_KEY_PATH before starting this — nothing shaped like a
|
||||||
|
// key/cert is committed here.
|
||||||
|
//
|
||||||
|
// Every TIN resolves to the same canned company — these specs don't care
|
||||||
|
// about per-TIN business logic, only that the lookup succeeds so the rest
|
||||||
|
// of the company-info form (name, address, manager) auto-fills instead of
|
||||||
|
// staying gated behind a "No matching business record" alert.
|
||||||
|
const https = require("node:https");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 443;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
key: fs.readFileSync(process.env.TLS_KEY_PATH || "/tmp/etrade-mock/key.pem"),
|
||||||
|
cert: fs.readFileSync(
|
||||||
|
process.env.TLS_CERT_PATH || "/tmp/etrade-mock/cert.pem",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
function companyInfo(tin) {
|
||||||
|
return {
|
||||||
|
Tin: tin,
|
||||||
|
LegalCondtion: "Private Limited Company",
|
||||||
|
RegNo: "REG-E2E-0001",
|
||||||
|
RegDate: "2020-01-01",
|
||||||
|
// Embeds the (per-run-unique) TIN rather than a static name — specs
|
||||||
|
// that look a company up by name (e.g. onboarding.cy.ts) need this to
|
||||||
|
// stay unique across repeated e2e runs against the same warm DB, same
|
||||||
|
// as it would be if the customer had typed a real company name.
|
||||||
|
BusinessName: `E2E Mock Trading PLC ${tin}`,
|
||||||
|
BusinessNameAmh: "ኢቱኢ ሞክ ትሬዲንግ",
|
||||||
|
PaidUpCapital: 100000,
|
||||||
|
AssociateShortInfos: [],
|
||||||
|
Businesses: [
|
||||||
|
{
|
||||||
|
MainGuid: "e2e-guid-0001",
|
||||||
|
OwnerTIN: tin,
|
||||||
|
DateRegistered: "2020-01-01",
|
||||||
|
TradeNameAmh: "ኢቱኢ",
|
||||||
|
TradesName: "E2E Mock Trading",
|
||||||
|
LicenceNumber: "LIC-E2E-0001",
|
||||||
|
RenewalDate: "2026-01-01",
|
||||||
|
RenewedFrom: "2025-01-01",
|
||||||
|
RenewedTo: "2027-01-01",
|
||||||
|
BusinessLicensingGroupMain: null,
|
||||||
|
SubGroups: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function businessInfo(tin) {
|
||||||
|
return {
|
||||||
|
MainGuid: "e2e-guid-0001",
|
||||||
|
OwnerTIN: tin,
|
||||||
|
DateRegistered: "2020-01-01",
|
||||||
|
TradeName: "E2E Mock Trading",
|
||||||
|
LicenceNumber: "LIC-E2E-0001",
|
||||||
|
Status: 1,
|
||||||
|
StatusDescription: "Active",
|
||||||
|
Capital: 100000,
|
||||||
|
AssociateShortInfos: [
|
||||||
|
{
|
||||||
|
Position: "Manager",
|
||||||
|
ManagerName: "አበበ በቀለ",
|
||||||
|
ManagerNameEng: "Abebe Bekele",
|
||||||
|
Photo: null,
|
||||||
|
MobilePhone: "+251911223344",
|
||||||
|
RegularPhone: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
AddressInfo: {
|
||||||
|
Region: "ADDIS ABABA",
|
||||||
|
Zone: "Zone 1",
|
||||||
|
Woreda: "Woreda 1",
|
||||||
|
Kebele: "Kebele 1",
|
||||||
|
HouseNo: "123",
|
||||||
|
MobilePhone: "+251911223344",
|
||||||
|
RegularPhone: "",
|
||||||
|
},
|
||||||
|
RenewedTo: "2027-01-01",
|
||||||
|
RenewedToDateString: "2027-01-01",
|
||||||
|
RenewalDate: "2026-01-01",
|
||||||
|
RenewedFrom: "2025-01-01",
|
||||||
|
CancellationDate: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = https.createServer(options, (req, res) => {
|
||||||
|
const url = new URL(req.url, `https://${req.headers.host}`);
|
||||||
|
|
||||||
|
const regMatch = url.pathname.match(
|
||||||
|
/^\/api\/Registration\/GetRegistrationInfoByTin\/([^/]+)\/en$/,
|
||||||
|
);
|
||||||
|
if (req.method === "GET" && regMatch) {
|
||||||
|
res.writeHead(200, { "content-type": "application/json" });
|
||||||
|
res.end(JSON.stringify(companyInfo(regMatch[1])));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.method === "GET" &&
|
||||||
|
url.pathname === "/api/BusinessMain/GetBusinessByLicenseNo"
|
||||||
|
) {
|
||||||
|
const tin = url.searchParams.get("Tin") || "";
|
||||||
|
res.writeHead(200, { "content-type": "application/json" });
|
||||||
|
res.end(JSON.stringify(businessInfo(tin)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404).end();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`etrade-mock listening on ${PORT}`);
|
||||||
|
});
|
||||||
52
e2e/freight/fayda-mock/server.js
Normal file
52
e2e/freight/fayda-mock/server.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// Minimal eSignet stand-in for the e2e stack. freight-api-e2e's
|
||||||
|
// FAYDA_TOKEN_ENDPOINT / FAYDA_USERINFO_ENDPOINT point here instead of the
|
||||||
|
// real (unreachable) Fayda infrastructure. It does not validate the
|
||||||
|
// client_assertion, code, or code_verifier it's sent — Cypress drives the
|
||||||
|
// verification via the API directly (POST start, then POST complete with a
|
||||||
|
// throwaway code), never through the real popup+SMS flow, so nothing here
|
||||||
|
// needs to be cryptographically real, only shaped like a working OIDC token
|
||||||
|
// + userinfo response.
|
||||||
|
const http = require("node:http");
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 4400;
|
||||||
|
|
||||||
|
const USERINFO = {
|
||||||
|
sub: "e2e-fayda-sub-0001",
|
||||||
|
name: "Abebe Bekele",
|
||||||
|
"name#en": "Abebe Bekele",
|
||||||
|
email: "abebe.bekele@example.com",
|
||||||
|
phone_number: "+251911223344",
|
||||||
|
gender: "Male",
|
||||||
|
"gender#en": "Male",
|
||||||
|
birthdate: "1990-01-01",
|
||||||
|
"address#en": "Bole Sub City, Addis Ababa, Ethiopia",
|
||||||
|
};
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
// Drain the body so the client's request completes cleanly even though we
|
||||||
|
// never read it (POST /token sends a form-urlencoded body we don't parse).
|
||||||
|
req.on("data", () => {});
|
||||||
|
req.on("end", () => {
|
||||||
|
if (req.method === "POST" && req.url.startsWith("/token")) {
|
||||||
|
res.writeHead(200, { "content-type": "application/json" });
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
access_token: "e2e-mock-access-token",
|
||||||
|
token_type: "Bearer",
|
||||||
|
expires_in: 3600,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.method === "GET" && req.url.startsWith("/userinfo")) {
|
||||||
|
res.writeHead(200, { "content-type": "application/json" });
|
||||||
|
res.end(JSON.stringify(USERINFO));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeHead(404).end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`fayda-mock listening on ${PORT}`);
|
||||||
|
});
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { execFileSync, spawnSync } from "node:child_process";
|
import { execFileSync, spawnSync } from "node:child_process";
|
||||||
|
import { generateKeyPairSync } from "node:crypto";
|
||||||
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import { createServer } from "node:net";
|
import { createServer } from "node:net";
|
||||||
import { dirname, join, resolve } from "node:path";
|
import { dirname, join, resolve } from "node:path";
|
||||||
@@ -110,6 +111,24 @@ async function resolvePorts() {
|
|||||||
return ports;
|
return ports;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throwaway RSA JWK for FAYDA_PRIVATE_KEY_BASE64 — signs a client_assertion
|
||||||
|
* JWT that fayda-mock-e2e never verifies (see docker-compose.e2e.yaml), so
|
||||||
|
* a fresh one every launch is fine; nothing needs it to stay stable across
|
||||||
|
* runs the way ports do. Generated here instead of committed as a literal
|
||||||
|
* blob in the compose file so nothing that looks like a private key sits in
|
||||||
|
* git history, even though this one only unlocks a local mock.
|
||||||
|
*/
|
||||||
|
function fakeFaydaPrivateKeyBase64() {
|
||||||
|
const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
|
||||||
|
const jwk = privateKey.export({ format: "jwk" });
|
||||||
|
jwk.kty = "RSA";
|
||||||
|
jwk.use = "sig";
|
||||||
|
jwk.alg = "RS256";
|
||||||
|
jwk.kid = "e2e-fayda-mock";
|
||||||
|
return Buffer.from(JSON.stringify(jwk)).toString("base64");
|
||||||
|
}
|
||||||
|
|
||||||
function envFor(ports) {
|
function envFor(ports) {
|
||||||
return {
|
return {
|
||||||
...process.env,
|
...process.env,
|
||||||
@@ -120,6 +139,8 @@ function envFor(ports) {
|
|||||||
CYPRESS_API_URL: `http://localhost:${ports.E2E_API_PORT}`,
|
CYPRESS_API_URL: `http://localhost:${ports.E2E_API_PORT}`,
|
||||||
CYPRESS_PORTAL_URL: `http://localhost:${ports.E2E_PORTAL_PORT}`,
|
CYPRESS_PORTAL_URL: `http://localhost:${ports.E2E_PORTAL_PORT}`,
|
||||||
E2E_DB_URL: `postgres://edr_e2e:edr_e2e@localhost:${ports.E2E_DB_PORT}/edr_freight_e2e`,
|
E2E_DB_URL: `postgres://edr_e2e:edr_e2e@localhost:${ports.E2E_DB_PORT}/edr_freight_e2e`,
|
||||||
|
FAYDA_PRIVATE_KEY_BASE64:
|
||||||
|
process.env.FAYDA_PRIVATE_KEY_BASE64 ?? fakeFaydaPrivateKeyBase64(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user