Skip to content

Local dev + end-to-end

getting-started.md covers cloning the monorepo and running a single service under uv (or a single frontend under npm). This page picks up where that stops: bringing up multiple services together against a real MySQL + Redis and driving the stack with Playwright — the same compose stack CI runs on every PR into cashless-v2.

For the deepest reference, see E2E.md in the parent repo. This page is the docs-site mirror.

When to use what

Scenario Tool
Editing one Python service in isolation uv per-service — see getting-started
Editing one Angular frontend in isolation npm run start per-service
Touching cross-service contracts (cm_v2 ↔ auth ↔ pa_v4) docker-compose.e2e.yml + Playwright
Reproducing a CI failure make e2e-up && make e2e-test
Validating a schema migration end-to-end make e2e-up (alembic runs in-stack)

The compose stack is the only path that boots cashless-auth + cm_v2 + pa_v4 + admin-ui + personalaccount-ui + marketplace-user-ui + MySQL + Redis wired together on a shared network with alembic head applied and fixtures seeded.

Topology

flowchart LR
    subgraph host["host machine"]
        PW["Playwright runner<br/>e2e/ (npx playwright test)"]
    end
    subgraph net["docker network: cashless-e2e"]
        MIG[(migrate<br/>alembic head)]
        MYSQL[("mysql:8.0<br/>:3309 -> :3306")]
        REDIS[("redis:7-alpine<br/>:6380 -> :6379")]
        AUTH["cashless-auth<br/>:8000"]
        CM["cashless-backend cm_v2<br/>:8080 -> :8090"]
        PA["personalaccount-api pa_v4<br/>:8082"]
        ADMIN["cashless-admin-ui<br/>:4201 -> :80"]
        PAUI["personalaccount-ui<br/>:4202 -> :80"]
        MKT["marketplace-user-ui<br/>:4203 -> :80"]
        VOL[("named volume:<br/>cashless-e2e-mysql-data")]
    end
    MYSQL --- VOL
    MIG --> MYSQL
    AUTH --> MYSQL
    AUTH --> REDIS
    CM --> MYSQL
    CM --> AUTH
    PA --> MYSQL
    PA --> AUTH
    ADMIN --> CM
    ADMIN --> AUTH
    PAUI --> PA
    PAUI --> AUTH
    MKT --> CM
    MKT --> AUTH
    PW --> ADMIN
    PW --> PAUI
    PW --> MKT
    PW --> MYSQL

The Playwright runner lives on the host and reaches services through the published host ports. The mysql fixtures helper (e2e/tests/fixtures/db.ts) connects to MySQL on 127.0.0.1:3309 for direct side-effect assertions (e.g. audit_log writes).

Stack inventory

Source: docker-compose.e2e.yml.

Service Host port Image / build
mysql 3309 mysql:8.0 (utf8mb4, STRICT_TRANS_TABLES)
redis 6380 redis:7-alpine
migrate python:3.12-slimalembic -x url=... upgrade head
cashless-auth 8000 services/cashless-auth/Dockerfile
cashless-backend 8080 backend/cashless-backend/Dockerfile.v2 (cm_v2, internal 8090)
personalaccount-api 8082 backend/personalaccount-api/Dockerfile.v4 (pa_v4)
cashless-admin-ui 4201 frontend/cashless-admin-ui/dockerfiles/Dockerfile
personalaccount-ui 4202 frontend/personalaccount-ui/dockerfiles/alpha.Dockerfile
marketplace-user-ui 4203 frontend/marketplace-user-ui/dockerfiles/Dockerfile

migrate is a one-shot job — every app service waits on migrate: service_completed_successfully before booting, so the schema is always at head when uvicorn starts.

The cm_v2 host-port 8080 is intentional: cashless-admin-ui's environment.ts bakes api_url_admin: http://localhost:8080, and the container maps 8080:8090 so the bundled SPA talks to the new backend with no rebuild.

Webhook secrets are placeholders

ADYEN_WEBHOOK_HMAC_KEY, MERCADO_PAGO_WEBHOOK_SECRET_KEY, and STRIPE_WEBHOOK_SECRET in docker-compose.e2e.yml are fixed test values so pa_v4 boots and the Adyen webhook spec can sign a deterministic payload offline. Any real Stripe / MP webhook delivery will 401 — don't point a tunnel at this stack expecting signature verification to pass.

Fixtures

Seed data is loaded by scripts/e2e/seed.py, invoked by scripts/e2e/bootstrap.sh after the stack is healthy. Every helper is idempotent on its natural key — re-running make e2e-up wipes the mysql volume and reseeds to the exact same state.

Row(s) inserted Identifier Notes
Admin (superadmin) admin@e2e.test / e2e-admin-pw role=admin, event_ids=NULL → all events
Admin (tenant) tenant@e2e.test / e2e-tenant-pw role=tenant, event_ids='1' → event 1 only
Event 1 E2E Test Event USD, capacity 100, status=1
Event 2 E2E Forbidden Event Cross-tenant negative case
Customer × 2 customer@e2e.test, customer2@e2e.test password e2e-customer-pw, US
UserChip × 2 E2E-NFC-001, E2E-NFC-002 Both ACTIVE on event 1
StripeCustomer + …ConnectPM customer@e2e.testpm_card_visa stripe_account_id='acct_e2e_placeholder'
Refund (in-scope) refundee@e2e.test Event 1, status=0
Refund (forbidden) forbidden-refundee@e2e.test Event 2, used by admin-tenant-scope.spec.ts

fixtures/ at the repo root is currently empty

/home/nex/cashless/fixtures/ exists as a placeholder but ships no files today. Authoritative seed lives in scripts/e2e/seed.py — the fixtures/ directory is reserved for future static-file fixtures (PSP webhook payloads, signed JWTs, sample uploads) and should be treated as TBD until populated.

_upsert_payment_method(vendor='mercadopago' | 'adyen') raises NotImplementedError — those PSP rows are tracked under E2E.md → What still needs to land.

Playwright project

/home/nex/cashless/e2e/ is a standalone npm project pinned to @playwright/test ^1.58.2. It deliberately does not declare a webServerbootstrap.sh owns the stack lifecycle so docker compose errors surface cleanly rather than being swallowed by the Playwright runner.

Project Tests
web admin-refund-bulk, admin-refund-bulk-audit, admin-tenant-scope, webhook-adyen, top-up-web (skip), marketplace-ticket-purchase (skip)
mobile-bridge mpos-transfer-confirm (skip), fastpass-create (skip)

Both projects run in Desktop Chrome only. CI installs Chromium via npx playwright install --with-deps chromium; the html reporter writes to e2e/playwright-report/. workers: 1 and fullyParallel: false — specs share a single MySQL and must not race.

Auth pattern: the web project logs in through the admin-ui form, then specs read cm.auth out of localStorage and reuse the bearer token via Playwright's request fixture. The dbConnection / dbQuery / dbExec / dbAuditLog fixtures in e2e/tests/fixtures/db.ts let a spec assert on audit_log and other side-effect tables directly.

Bringup recipe

From a clean checkout (parent monorepo, cashless-v2 branch, submodules initialized per getting-started):

make e2e-up       # build, start, alembic head, seed fixtures
cd e2e
npm ci
npx playwright install --with-deps chromium
npm test          # runs all six 'web' + two 'mobile-bridge' specs

After the first run the build cache is warm; subsequent make e2e-up finishes in ~30s. The four test.skip skeletons report as skipped, not failed.

Useful one-liners:

make e2e-test                       # re-run Playwright suite
make e2e-logs                       # tail every container
cd e2e && npm run test:headed       # watch the browser drive
cd e2e && npm run report            # open last html report
docker compose -f docker-compose.e2e.yml ps

Tearing down + resetting state

make e2e-down                       # stop, drop mysql volume
docker volume rm cashless-e2e-mysql-data   # belt-and-braces wipe
make e2e-up                         # rebuild + reseed

bootstrap.sh always calls compose down -v --remove-orphans before compose up, so make e2e-up is the canonical reset path — make e2e-down is only needed when freeing the ports for a different workflow.

Don't share the mysql volume across stacks

The named volume cashless-e2e-mysql-data is scoped to this compose project. If you start the stack with a different project name (e.g. docker compose -p foo -f docker-compose.e2e.yml up), a fresh volume is created and migrate will run against an empty schema — your seeded admins won't be there. Stick with make e2e-*.

Troubleshooting

Port already in use (4201 / 4202 / 4203 / 8000 / 8080 / 8082 / 3309 / 6380). Stop the local Angular dev server or uvicorn first, or remap the published port in docker-compose.e2e.yml. The admin-ui's baked api_url_admin is http://localhost:8080 — if you remap cm_v2 you must rebuild the admin-ui image with a matching environment.ts.

make e2e-up hangs at --wait. A container failed its healthcheck. Run docker compose -f docker-compose.e2e.yml ps to see which, then docker compose -f docker-compose.e2e.yml logs <service>. Most common: MySQL still cold when migrate fires (bumped start_period should cover it; raise mysql healthcheck retries if it flaps), or pa_v4 boot fails because the placeholder webhook secrets were removed.

Login redirects back to /login. Seed didn't run. docker logs cashless-e2e-migrate should end with Running upgrade ... -> head; if it errored, fix the migration and re-run make e2e-up.

POST /v2/refunds/bulk → 401. Access token expired mid-test (60min in compose env, 15min in prod). Just re-run — Playwright re-logs in on each invocation.

Slow first-up (several minutes). Expected. The migrate container does an apt-get install for the MySQL client libs before pip install -e . against core/models. Subsequent runs reuse the buildx cache. CI pre-warms via the actions/cache@v4 step on /tmp/.buildx-cache.

Missing secret in local env. Compose pins every required env inline — there is no .env.e2e file you need to create. If a real PSP key is required for a journey you're enabling, override at invocation time:

STRIPE_WEBHOOK_SECRET=whsec_real \
  docker compose -f docker-compose.e2e.yml up -d

CI parity

The compose stack you run locally is exactly what .github/workflows/e2e.yml runs on every PR into cashless-v2. The workflow:

  1. Checks out with submodules: recursive.
  2. Caches docker buildx layers.
  3. npm ci + npx playwright install --with-deps chromium under e2e/.
  4. make e2e-upmake e2e-testmake e2e-down.
  5. Uploads e2e/playwright-report/ as an artifact on failure.

continue-on-error: true is set on the test step today — the gate flips to blocking once the four test.skip journey skeletons turn green. See E2E.md → CI gate posture.

See also