Files
edr-platform/docker-compose.e2e.yaml
Nathnael 3a69b961d4 refactor(freight): move the Fayda callback to /fayda/callback
Namespaces the OAuth landing path in all three places it exists: the API's
ack controller, both web apps' routes, and the redirect_uri env values.
A bare /callback claimed a generic top-level path in every app for one
provider's redirect.

The API side needed care. The ack controller moves to @Controller
('fayda/callback'), and the global-prefix exclusion has to name that exact
path — setGlobalPrefix's exclude is an exact route match, not a subtree, so
excluding "fayda" would have left /fayda/callback served at
/api/fayda/callback and 404ing at the registered redirect_uri, while
reading as though it covered everything under /fayda. Naming the full path
also keeps /api/fayda/verification/* prefixed, which every client calls.

Also drops a stale comment on the portal's callback route describing the
popup that no longer exists, and records why the route is public: behind
RequireAuth the onboarding gate redirects to /portal before the code+state
exchange can run.

NOT verified at runtime — this changes route registration, so boot the API
and confirm GET /fayda/callback answers un-prefixed and
/api/fayda/verification/start still resolves before relying on it.

Deploying this requires registering the new redirect_uri with eSignet
first; FAYDA_WEB_REDIRECT_URI, FAYDA_PORTAL_REDIRECT_URI and any mobile
client must be updated in step or verification breaks with a redirect_uri
mismatch.
2026-08-04 12:43:25 +00:00

335 lines
12 KiB
YAML

# 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