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-slim → alembic -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.test ↔ pm_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
webServer — bootstrap.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:
CI parity¶
The compose stack you run locally is exactly what
.github/workflows/e2e.yml
runs on every PR into cashless-v2. The workflow:
- Checks out with
submodules: recursive. - Caches docker buildx layers.
npm ci+npx playwright install --with-deps chromiumundere2e/.make e2e-up→make e2e-test→make e2e-down.- 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¶
E2E.md— the canonical reference.scripts/e2e/bootstrap.sh— lifecycle script invoked bymake e2e-up.scripts/e2e/seed.py— fixture seed.e2e/playwright.config.ts— Playwright config.- Getting started — single-service
uv/npmdev loop. - Architecture — service boundaries and deploy flow.