Files
edr-platform/e2e-ui/README.md

155 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# EDR Passenger — Playwright UI E2E
Browser E2E for the passenger platform. **Track A** = portal booking combinations; **Track B** =
backoffice config → portal propagation. Scenario matrix: `docs/ui-e2e-test-matrix.md`.
## Status
**Track A + Track B implemented and green** — 27 passing specs, 1 documented skip (UA-7). Full suite
runs deterministically in ~1.8 min (`workers:1`, one seeded DB shared serially). The whole booking
flow is factored into `fixtures/booking-flow.ts``bookTrip(page, opts)` drives an arbitrary
passenger mix, nationality, trip type, promo, and payment method end to end (search → select →
passengers → seats → review → pay → confirmation), capturing the price at each hop; `bookOneAdult` is
a thin back-compat wrapper.
### Coverage vs `docs/ui-e2e-test-matrix.md`
**Track A — booking combinations** (`specs/portal`, `specs/guest`):
| ID | Spec | What it proves |
|----|------|----------------|
| UA-1 | `ua1` | one-way 1A ETB WALLET — full money chain equal, CONFIRMED |
| UA-1b | `ua1b-usd-divergence` | 🔴 USD card display fare diverges 100× from internal `baseFareMinor` |
| UA-2 | `ua2-usd-booking` | 🔴 USD booking — reviewed total (ETB) vs stored total (USD) diverge 100× |
| UA-3 | `ua3-djf` | DJF booking settles via forged gateway payment |
| UA-3w | `ua3-djf` | 🔴 DJF WALLET — reviewed (DJF) vs stored (ETB) diverge |
| UA-4 | `ua4-child-free` | first child <5 free total = one adult fare, free child not seated |
| UA-5 | `ua5-second-child-paid` | 1A+2C second child pays full fare (2 seats) |
| UA-6 | `ua6-round-trip` | 🔴 round-trip inbound leg has availability but no priced coach (booking blocked) |
| UA-8 | `ua8-promo-drop` | valid promo now applied server-side; booking stored at the discounted total (H-13 fixed & guarded) |
| UA-11 | `ua11-expired-promo` | expired promo ignored full fare booked |
| UA-13 | `ua13-forged-total` | client-forged `reviewedTotalMinor=1` now REJECTED 4xx, nothing stored (C-1 fixed & guarded) |
| UA-14 | `ua14-forged-seat-fare` | guest forged `seatFareMinor=0` now REJECTED 4xx, nothing stored (C-1 fixed & guarded) |
| UA-15 | `ua15-telebirr-shortpay` | short-paid gateway settlement now REFUSED booking stays unconfirmed (C-4 fixed & guarded) |
| UA-16 | `ua16-family-mix` | 2A+3C two children free, one paid (3 seats) |
**Track B — config → portal propagation & validation gaps** (`specs/backoffice`, `specs/propagation`):
| ID | Spec | What it proves |
|----|------|----------------|
| PB-1 | `pb-config-propagation` | FX-rate change propagates live to portal USD pricing |
| PB-2 / PB-2b | `pb-config-propagation` | seat-class base-price change propagates live; `basePrice` field drives the fare |
| PB-4 | `pb-config-propagation` | station added in backoffice appears in the portal station list |
| PB-7 | `config-validation` | 🔴 promo created with backoffice UI field names is inert (field-name mismatch) |
| PB-10 | `pb-config-propagation` | deleting an FX rate now FAILS CLOSED (no priced fare) instead of a silent 1.0 collapse (M-5/H-2 fixed & guarded) |
| BC-7 | `pb-config-propagation` | negative seat-class base price now REJECTED (400, DTO `@Min(0)`) (M-1 fixed & guarded) |
| BC-8 | `config-validation` | promo over 100% now REJECTED (400, DTO `@Max(100)`) (M-2 fixed & guarded) |
| BC-9 | `config-validation` | negative seat-hold duration now REJECTED (400, whitelisted typed `/config` DTO) (M-3 fixed & guarded) |
| BC-10 | `config-validation` | 🔴 schedule with a past departure accepted (M-4) |
| BC-11 | `pb-config-propagation` | non-admin passenger now FORBIDDEN (403) from FX writes; admin still allowed (C-8 fixed & guarded) |
### Deferred (documented, not silently omitted)
- **UA-7** (round-trip berth) `specs/portal/ua7-berth.spec.ts` is `test.skip` with the reason: needs
a bed CoachType + reverse-leg pricing, both backend prerequisites (see UA-6 for the reverse-leg gap).
- **UA-9 / UA-10 / UA-12 / UA-17** the matrix moves these to the API-level harness (over-100% /
over-subtotal promos, loyalty over-redeem, DJF×promo negative total): no reachable browser path
(server clamps `reviewedTotalMinor` 0; the portal never calls the loyalty/fare-quote path).
- **PB-3/5/6/8/9, BC-1BC-6** additional config surfaces and delete-referenced/mid-flight/staleness
variations of the finding classes already covered above; the matrix marks several as deferrable.
**Gateway settlement:** the real telebirr gateway is unreachable in the test env (`/payments/initiate`
502s), so gateway rows (UA-3, UA-15) create the booking through the real browser flow and then inject
settlement via `POST /internal/payments/mark-paid` exactly the matrix's settlement-injection plan.
Portal testids used: `result-select-btn`, `coach-option`, `continue-passenger-details`,
`pay-method-{TYPE}`. Everything else (passengers form + DOB modal, seats auto-assign, review, payment)
is driven via name/placeholder/role selectors no further source edits were needed.
**Seed note:** `Passenger.id` is set EQUAL to the IAM user id see the comment in `seed-ui.ts`
(`UI_IDS.passenger`) and the SUSPECTED FINDING below. Each coach seeds 48 seats so a full serial run
never exhausts availability across specs.
## Suspected finding (surfaced while building UA-1)
`POST /bookings` (authenticated) overrides `passengerId` with the JWT user id
(`bookings.controller.ts:528-532`, "never trust the request body"). The service only resolves an
iamUserId Passenger when it is **non-UUID** (`bookings.service.ts:773`). IAM user ids are UUIDs, so
the resolver never fires and `booking.create` uses the iamUserId directly as `passengerId` FK
violation unless `Passenger.id == iamUserId`. This is why the seed aligns them. **Verify against a
real IAM-authenticated booking** if `req.user.id` is genuinely the iamUserId in production,
authenticated portal bookings may be broken (guest path unaffected). Candidate for `docs/ISSUES.md`.
## Prerequisites — the running stack
The suite drives a live stack. `global-setup.ts` seeds + mints auth, but assumes the apps are
already up. Bring them up once (leave running across test runs):
```bash
# 1. Infra: test Postgres (5544) + RabbitMQ (5672, payment vhost)
bash e2e/prepare.sh # postgres + migrations
docker compose -f e2e/docker-compose.yml up -d rabbitmq-e2e
# 2. Build the shared types package (nest build needs the dist)
pnpm --filter @edr/types build
# 3. passenger-api on :4000 against the 5544 DB, with org+staff seeding on
# (apps/edr-passenger-api/.env sets DATABASE_URL=…5544, PORT=4000,
# RABBITMQ_ENABLED=false, FAYDA_ENABLED=false, SEED_EDR_PASSENGER_ORG=true,
# SEED_PASSENGER_STAFF=true, DEFAULT_PASSWORD=Test@1234)
( cd apps/edr-passenger-api && pnpm dev ) # background
# 4. Web apps (each has .env.local → NEXT_PUBLIC_API_URL=http://localhost:4000)
( cd apps/edr-passenger-web/portal && pnpm dev ) # :5174, background
( cd apps/edr-passenger-web/backoffice && pnpm dev ) # :5184, background
```
> `playwright.config.ts` now declares a `webServer` block that auto-boots api/portal/backoffice and
> **reuses** them if already running, so steps 34 are optional in local dev. Gateway rows settle via
> the internal `mark-paid` endpoint, so `apps/edr-payment-api` (:3003) is **not** required.
## Run
One command (infra build boot seed+auth run open report):
```bash
bash e2e-ui/run.sh # all projects; args pass through to playwright
bash e2e-ui/run.sh --headed # watch it in a real browser
SLOWMO=500 bash e2e-ui/run.sh --headed # slow every action by 500ms
bash e2e-ui/run.sh --project=portal ua4 # one project / filter by title
```
Or, against an already-running stack:
```bash
pnpm test:e2e:ui # all projects
pnpm test:e2e:ui -- --project=guest --project=backoffice # smoke only
```
HTML report `e2e-ui-report/index.html`.
## Layout
```
e2e-ui/
playwright.config.ts projects: portal (passenger auth), guest (none),
backoffice (staff auth), propagation (cross-app)
global-setup.ts seeds test DB (seed-ui.ts) + mints staff.json via real /login
fixtures/
data.ts station IDs, sample depart date, results deep-link helper
storage/staff.json generated staff storageState (gitignored)
specs/{guest,portal,backoffice,propagation}/*.spec.ts
```
Seed lives with the API harness: `apps/edr-passenger-api/test/fixtures/seed-ui.ts` (extends
`seed-core.ts` with a bookable Train/Schedule/Coach/Seats, enabled PaymentMethods WALLET+TELEBIRR,
promos, funded wallet). Run standalone: `npx ts-node test/fixtures/seed-ui.ts`.
## Auth model (grounded in the app)
- **Portal (passenger)**: `localStorage.auth_token` only, no server gate. (passenger storageState is
a Phase 3 item smoke uses the `guest` project.)
- **Backoffice (staff)**: middleware requires the `auth_token` **cookie**; API guards check the
session's permissions. `global-setup` logs in as the seeded `passenger.admin@edr.local` through
the real `/login` UI and snapshots both. No hand-crafted tokens.