# EDR Freight — ephemeral Cypress e2e stack. # Fully isolated from dev: own ports, own throwaway Postgres (tmpfs — data # vanishes on `down`), seeded test users. Requires the same .npmrc as the main # docker-compose.yaml (GitHub Packages auth for @tria-plc). # # Preferred entrypoint: the launcher (auto-up + free-port picking): # pnpm e2e:freight:run|open|ci|up|down → e2e/freight/scripts/e2e.mjs # # Host ports are env-parameterized (E2E_*_PORT). Defaults below avoid the dev # stacks (5273/5283/3221 are taken by the second dev checkout in # ~/projects/nathnael/edr-platform); when a default is busy the launcher scans # upward for a free port and remembers the choice in e2e/freight/.e2e-ports.json # while the stack is up: # freight-api 3101 portal 5373 backoffice 5383 # postgres 5533 minio 9310 (console 9311) name: edr-freight-e2e services: postgres-freight-e2e: image: postgres:16-alpine environment: POSTGRES_DB: edr_freight_e2e POSTGRES_USER: edr_e2e POSTGRES_PASSWORD: edr_e2e tmpfs: - /var/lib/postgresql/data ports: - "${E2E_DB_PORT:-5533}:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U edr_e2e -d edr_freight_e2e"] interval: 2s timeout: 3s retries: 30 minio-e2e: image: minio/minio:latest command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: e2e-minio MINIO_ROOT_PASSWORD: e2e-minio-secret tmpfs: - /data ports: - "${E2E_MINIO_PORT:-9310}:9000" - "${E2E_MINIO_CONSOLE_PORT:-9311}:9001" healthcheck: test: ["CMD", "mc", "ready", "local"] interval: 5s timeout: 5s retries: 12 # tmpfs wipes MinIO on every restart — recreate the app bucket each boot. minio-init-e2e: image: minio/mc:latest depends_on: minio-e2e: condition: service_healthy entrypoint: - /bin/sh - -c - mc alias set e2e http://minio-e2e:9000 e2e-minio e2e-minio-secret && mc mb --ignore-existing e2e/fhc restart: "no" # One-shot: runs schema creation + migrations against postgres-freight-e2e, # then exits. freight-api-e2e no longer migrates itself on boot (migrationsRun # is false) — this is the CI "migration" stage, run here the same way. freight-migration-e2e: build: context: . dockerfile: apps/edr-freight-api/Dockerfile target: migration secrets: - npmrc depends_on: postgres-freight-e2e: condition: service_healthy environment: DB_HOST: postgres-freight-e2e DB_PORT: "5432" DB_USER: edr_e2e DB_PASSWORD: edr_e2e DB_NAME: edr_freight_e2e 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 the payment microservice. The API's reconcile-before-expire # step (booking-batch.service.ts:3492) asks the gateway whether a late # payment landed before it will expire an unpaid hold, and treats ANY error # as "unverifiable" — which defers the expiry forever. PAYMENT_API_URL # otherwise defaults to the real paymentcallback.triaplc.com, unreachable # from here, so without this every expiry scenario hangs. See # payment-mock/server.js. payment-mock-e2e: image: node:20-alpine volumes: - ./e2e/freight/payment-mock:/app:ro working_dir: /app command: ["node", "server.js"] healthcheck: test: [ "CMD", "node", "-e", "fetch('http://localhost:4500/health').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: build: context: . dockerfile: apps/edr-freight-api/Dockerfile secrets: - npmrc depends_on: postgres-freight-e2e: condition: service_healthy minio-e2e: condition: service_healthy minio-init-e2e: condition: service_completed_successfully freight-migration-e2e: condition: service_completed_successfully fayda-mock-e2e: condition: service_healthy etrade-mock-e2e: condition: service_healthy payment-mock-e2e: condition: service_healthy environment: PORT: "3001" DB_HOST: postgres-freight-e2e DB_PORT: "5432" DB_USER: edr_e2e DB_PASSWORD: edr_e2e DB_NAME: edr_freight_e2e # e2e-only secrets — never reuse outside this stack JWT_SECRET: e2e-jwt-secret JWT_ACCESS_TOKEN_SECRET: e2e-access-secret JWT_REFRESH_TOKEN_SECRET: e2e-refresh-secret JWT_EXPIRES_IN: 1d JWT_ACCESS_TOKEN_EXPIRES: 1d JWT_REFRESH_TOKEN_EXPIRES: 7d SERVICE_AUTH_TOKEN: e2e-service-token # Org/unit/position boot seeders (env-gated in app code). Test USERS are # NOT seeded by the API — Cypress inserts them via # e2e/freight/cypress/fixtures/seed-users.sql before specs run. SEED_EDR_ORG: "true" SUPER_ADMIN_EMAIL: superadmin@tria.com SUPER_ADMIN_PHONE: "+251900000000" # Object storage MINIO_ENDPOINT: minio-e2e MINIO_PORT: "9000" MINIO_USE_SSL: "false" MINIO_ACCESS_KEY: e2e-minio MINIO_SECRET_KEY: e2e-minio-secret MINIO_REGION: us-east-1 # External integrations off RABBITMQ_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 # Without this the payment client calls the real (unreachable) # paymentcallback.triaplc.com and every unpaid hold defers instead of # expiring — see payment-mock/server.js. PAYMENT_API_URL: http://payment-mock-e2e:4500 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}/fayda/callback FAYDA_PORTAL_REDIRECT_URI: http://localhost:${E2E_PORTAL_PORT:-5373}/fayda/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 — # blackhole it so e2e never sends SMS (failures are logged, non-fatal). OZIKING_SMS_URL: http://127.0.0.1:9/sms FREIGHT_PORTAL_URL: http://localhost:${E2E_PORTAL_PORT:-5373} ports: - "${E2E_API_PORT:-3101}:3001" healthcheck: # Migrations run in freight-migration-e2e before this container even # starts (depends_on above) — boot here is just Nest bootstrap + seeders. test: [ "CMD", "node", "-e", "fetch('http://localhost:3001/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", ] interval: 5s timeout: 5s retries: 12 start_period: 60s freight-portal-e2e: build: context: . dockerfile: infrastructure/docker/Dockerfile.web args: TURBO_FILTER: "@edr/freight-portal" APP_PATH: apps/edr-freight-web/portal # Baked at build time: browser (host or host-networked cypress # container) reaches the API through the published host port. A # non-default API port therefore forces a web image rebuild. VITE_API_URL: http://localhost:${E2E_API_PORT:-3101} VITE_BASE_API_URL: http://localhost:${E2E_API_PORT:-3101} VITE_USER_MANAGEMENT_BASE: /_um VITE_GOOGLE_MAPS_API_KEY: "" VITE_POSTHOG_KEY: "" VITE_POSTHOG_HOST: "" secrets: - npmrc ports: - "${E2E_PORTAL_PORT:-5373}:80" freight-backoffice-e2e: build: context: . dockerfile: infrastructure/docker/Dockerfile.web args: TURBO_FILTER: "@edr/freight-backoffice" APP_PATH: apps/edr-freight-web/backoffice VITE_API_URL: http://localhost:${E2E_API_PORT:-3101} VITE_BASE_API_URL: http://localhost:${E2E_API_PORT:-3101} VITE_USER_MANAGEMENT_BASE: /_um VITE_GOOGLE_MAPS_API_KEY: "" VITE_POSTHOG_KEY: "" VITE_POSTHOG_HOST: "" secrets: - npmrc ports: - "${E2E_BACKOFFICE_PORT:-5383}:80" # Headless runner — opt-in via `--profile cypress`. host network so the # in-container browser uses the exact same localhost URLs as `cypress open` # on the host (Linux only; on macOS/Windows run Cypress from the host). cypress: # Keep in sync with the cypress version in e2e/freight/package.json. image: cypress/included:${CYPRESS_VERSION:-15.18.1} profiles: ["cypress"] network_mode: host depends_on: freight-api-e2e: condition: service_healthy working_dir: /repo/e2e/freight # NOTE: host network shares the abstract X-socket namespace with the host. # Cypress spawns its Xvfb on :99 — run only ONE cypress container at a # time, and don't run it on a host whose X server occupies :99. entrypoint: ["cypress", "run", "--browser", "chrome"] environment: CI: "true" E2E_DB_URL: postgres://edr_e2e:edr_e2e@localhost:${E2E_DB_PORT:-5533}/edr_freight_e2e CYPRESS_BASE_URL: http://localhost:${E2E_BACKOFFICE_PORT:-5383} CYPRESS_API_URL: http://localhost:${E2E_API_PORT:-3101} CYPRESS_PORTAL_URL: http://localhost:${E2E_PORTAL_PORT:-5373} # Must match freight-api-e2e's SERVICE_AUTH_TOKEN above. CYPRESS_SERVICE_AUTH_TOKEN: e2e-service-token volumes: - .:/repo secrets: npmrc: file: .npmrc