mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
- Implemented G9·S38 tests for customer self-haul truck assignments, ensuring compliance with container limits and truck assignments. - Added G9·S39 tests for last-mile delivery, self-haul, and yard pickup, verifying independent paths for multiple bookings on the same train. - Created seed data for Group 1 and Group 2 scenarios, ensuring proper setup for weight and capacity tests. - Updated booking interface to deprecate in favor of for better clarity in allocations.
170 lines
9.3 KiB
Markdown
170 lines
9.3 KiB
Markdown
# @edr/freight-e2e — Cypress e2e suite for the freight system
|
||
|
||
Containerized, fully isolated e2e environment: throwaway Postgres (tmpfs),
|
||
MinIO, freight-api, portal, and backoffice — plus a Cypress runner that works
|
||
both headless-in-Docker and interactively from the host against the same URLs.
|
||
|
||
## Stack (`docker-compose.e2e.yaml`, project name `edr-freight-e2e`)
|
||
|
||
| Service | Default port | Notes |
|
||
| ----------------------- | ------------ | ---------------------------------------------- |
|
||
| `freight-api-e2e` | 3101 | migrations + seeders run at boot |
|
||
| `freight-portal-e2e` | 5373 | nginx static build, API URL baked at build |
|
||
| `freight-backoffice-e2e`| 5383 | nginx static build, API URL baked at build |
|
||
| `postgres-freight-e2e` | 5533 | `edr_freight_e2e`, tmpfs — gone on `down` |
|
||
| `minio-e2e` | 9310/9311 | object storage for file features |
|
||
| `cypress` | (host net) | profile `cypress`, headless chrome |
|
||
|
||
Ports are env-parameterized (`E2E_API_PORT`, `E2E_PORTAL_PORT`,
|
||
`E2E_BACKOFFICE_PORT`, `E2E_DB_PORT`, `E2E_MINIO_PORT`,
|
||
`E2E_MINIO_CONSOLE_PORT`). Defaults avoid the dev stacks; if a default is
|
||
busy anyway, the launcher scans upward for a free port, remembers the choice
|
||
in `.e2e-ports.json` (gitignored) while the stack is up, and passes matching
|
||
URLs to both compose and Cypress. The dev database is never touched.
|
||
|
||
## Usage (from repo root)
|
||
|
||
One command — the launcher (`scripts/e2e.mjs`) auto-builds and starts the
|
||
stack if it isn't running, waits for healthchecks, then runs Cypress against
|
||
whatever ports were picked:
|
||
|
||
```bash
|
||
pnpm e2e:freight:run # headless run from the host (auto-up)
|
||
pnpm e2e:freight:open # interactive Cypress on the host (auto-up)
|
||
pnpm e2e:freight:ci # headless run inside the cypress container (auto-up)
|
||
pnpm e2e:freight:up # just start the stack
|
||
pnpm e2e:freight:down # teardown, drop all data + forget ports
|
||
pnpm e2e:freight:run --spec 'cypress/e2e/flows/**' # extra args → cypress
|
||
```
|
||
|
||
First `up` is slow (image builds + 240 migrations + seeders — healthcheck
|
||
allows 3 min). Later runs against a live stack skip docker entirely. Requires
|
||
the same root `.npmrc` (GitHub Packages auth for `@tria-plc`) as the main
|
||
compose file. Note: a non-default API port forces a web-image rebuild (the
|
||
API URL is baked into the static builds).
|
||
|
||
The `cypress` service uses `network_mode: host` (Linux). On macOS/Windows run
|
||
Cypress from the host (`e2e:freight:open` / `e2e:freight:run`) instead of the
|
||
container.
|
||
|
||
## Test users
|
||
|
||
Inserted by Cypress itself — a global `before()` hook runs
|
||
`cy.task("db:seedUsers")`, which executes `cypress/fixtures/seed-users.sql`
|
||
then `cypress/fixtures/seed-company.sql` (idempotent, pre-hashed argon2
|
||
passwords) against the e2e database. No API code is involved; the app's user
|
||
seeders stay disabled. The API's always-on boot seeders must have run first
|
||
(org/unit/positions) — guaranteed once `freight-api-e2e` is healthy.
|
||
|
||
- Staff (backoffice): `linestaff|chief|director|ceo|marketer|operation|gl-et|gl-dj@edr.local`
|
||
— password `password@tria`
|
||
- Customers (portal): `user@gmail.com`, `user2@gmail.com`
|
||
— password `12345678`
|
||
|
||
`seed-company.sql` additionally gives `user@gmail.com` an ACTIVE company
|
||
("E2E Logistics PLC", TIN `0102030405`) with an approved importer profile —
|
||
the contract wizard's precondition — and grants `chief` the
|
||
`edr_freight_app:admin` permission (customer-profile approval is
|
||
FreightAdmin-guarded and no seeded position carries it otherwise).
|
||
|
||
Full map in `cypress/fixtures/users.json`.
|
||
|
||
## Conventions
|
||
|
||
- **Programmatic login** everywhere except the two dedicated UI-login specs:
|
||
`cy.loginBackoffice(email?)` / `cy.loginPortal(email?)` — `cy.session`-cached
|
||
(across specs), `POST /api/auth/login`, sets the `auth-token` /
|
||
`refresh-token` cookies the apps read.
|
||
- **Origins**: `baseUrl` is the backoffice (5383). Portal specs `cy.visit`
|
||
the absolute portal URL; a test that touches *both* apps wraps portal steps
|
||
in `cy.origin()` (different port = different origin). Cookies ignore ports —
|
||
always call the matching login command right before switching apps so
|
||
`cy.session` restores the right cookie snapshot.
|
||
- **DB access**: `cy.task("db:query", { sql, params })` runs SQL against the
|
||
e2e database (`E2E_DB_URL`, default `localhost:5533`). Use for seeding
|
||
edge-case data and asserting side effects — it can never reach the dev DB.
|
||
- **OTPs**: SMS/email delivery is disabled in e2e, but codes are still stored
|
||
in `freight.otp_verifications` — `cy.getOtp(emailOrPhone)` polls them out.
|
||
Used by signup verification and contract customer-signing.
|
||
- **Spec layout**:
|
||
- `cypress/e2e/api/` — API contract via `cy.request` (no browser)
|
||
- `cypress/e2e/backoffice/` — staff app
|
||
- `cypress/e2e/portal/` — customer app
|
||
- `cypress/e2e/flows/` — cross-app journeys (both directions):
|
||
- `onboarding.cy.ts` — signup → OTP → wizard (docs + license upload) →
|
||
backoffice approval → customer can contract
|
||
- `contract-lifecycle.cy.ts` — wizard → submit → accept → 2-step approval
|
||
→ PDF → customer OTP-sign → staff counter-sign → `CONTRACT_ACTIVE`
|
||
- **Journey specs** (`flows/onboarding`, `flows/contract-lifecycle`) run with
|
||
`retries: 0` and resolve mid-journey state (user, company, contract) from
|
||
the DB at the start of each test: switching origin between tests reloads
|
||
the spec bundle, so module-level variables do NOT survive across tests.
|
||
|
||
### The 40-scenario suite (`flows/g1_*` … `flows/g10_*`)
|
||
|
||
S1–S40 from the scenario document, one file per group after Group 1:
|
||
|
||
| File | Scenarios | Subject |
|
||
| --- | --- | --- |
|
||
| `g1_s1_expiry_promotes_waitlist.cy.ts` | S1 | expiry frees exactly the waitlist's space |
|
||
| `g1_s2_exact_fill.cy.ts` | S2 | four bookings fill the train to the slot |
|
||
| `g1_s3_underfill_day_stays_open.cy.ts` | S3 | under-filled day stays bookable |
|
||
| `g1_s4_split_closes_gap.cy.ts` | S4 | a split closes the last gap |
|
||
| `g1_s5_cascading_expiry.cy.ts` | S5 | one expiry cascades into a second promotion |
|
||
| `g1_s6_s8_offers_and_priority.cy.ts` | S6–S8 | declined split, government preemption, priority tiers |
|
||
| `g2_weight.cy.ts` | S9–S12 | weight vs slots, and the tolerance rules |
|
||
| `g3_export.cy.ts` | S13–S18 | export FCFS, whole-or-nothing |
|
||
| `g4_multi_schedule.cy.ts` | S19–S21 | two trains on one day |
|
||
| `g5_waitlist.cy.ts` | S22–S24 | recovery: rebooking, split remainders, queue walking |
|
||
| `g6_corridor.cy.ts` | S25–S29 | the run: alighting, tracking, checkpoints |
|
||
| `g7_disruptions.cy.ts` | S30–S33 | cancel, wagon shortage, breakage, under-filled dispatch |
|
||
| `g8_import_customs.cy.ts` | S34–S37 | the clearance chain |
|
||
| `g9_delivery.cy.ts` | S38–S39 | self-haul trucks and last-mile |
|
||
| `g10_validation.cy.ts` | S40 | line validation and the parked re-priced booking |
|
||
|
||
**Read `SCENARIO_ENGINE_NOTES.md` before changing any of these.** Five
|
||
scenarios describe behaviour the engine does not implement (out-of-order
|
||
checkpoints, second-duty gating, hazardous/reefer clamping) or invert what it
|
||
does (mid-corridor intercity). Those are written as a passing test of CURRENT
|
||
behaviour plus an adjacent `it.skip` naming the desired behaviour — un-skipping
|
||
one is the definition of done for the corresponding fix, not a test repair.
|
||
|
||
Three specs are also flag- or policy-dependent and say so in their headers:
|
||
`g3_export` needs `FREIGHT_EXPORT_SPLIT` off, and `g4_multi_schedule` asserts
|
||
the whole-placement policy (S20) rather than the fill-first one (S21).
|
||
|
||
### Group 1 conventions (`flows/g1_*.cy.ts`)
|
||
|
||
The visual counterpart to the corridor suite — helpers in `flows/g1-utils.ts`,
|
||
arrange-data in `fixtures/seed-g1-train.sql` (run it AFTER
|
||
`seed-import-corridor.sql`).
|
||
|
||
Two things make these different from the older flow specs:
|
||
|
||
- **A 53-wagon BUILT train** (`TRN-G1-1`), not a loco pair. A loco-pair
|
||
schedule cannot hold 53: `syncScheduleMaxWagons` recomputes `max_wagons`
|
||
from locomotive length (`floor(760 / 13.966) = 54` on this corridor). A
|
||
built train's physical consist wins outright — see
|
||
`booking-batch.service.ts:4152`. The consist staff marshal IS the capacity.
|
||
- **The configuration phase and every capacity verdict run through the UI**:
|
||
the consist is seen in the Train Builder, the schedule is created through
|
||
the real "New schedule" form, the batch is run from the
|
||
`Doc review complete — run batch` button, and FULL/NOT FULL is read off the
|
||
batch board's Priority Tracking tab — which renders the literal
|
||
`Capacity line · 53/53 wagons · FULL` divider plus `In the batch` /
|
||
`Waiting list` / `Expired` lanes.
|
||
|
||
Bulk cargo still goes through the API (`bookContainers`): a 30-wagon booking
|
||
is 30-60 ISO-number inputs, which tests the form rather than the engine. Each
|
||
scenario books its ONE small booking visually via
|
||
`bookContainersVisually()`. **Payment is always API-driven** — the portal has
|
||
no mock payment path; "Pay now" redirects off-origin to a real gateway, which
|
||
Cypress cannot follow.
|
||
|
||
## Extending
|
||
|
||
Deep module flows (booking wizard → staff approval → scheduling → billing)
|
||
belong in `flows/`. Pattern: arrange via API/`db:query`, act through the UI of
|
||
one app, assert through the UI of the other + a `db:query` cross-check. Prefer
|
||
adding `data-testid` attributes to app code over brittle text selectors.
|