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