# @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. ## 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.