Implements the full E2E flow against the deployed docker stack and
documents it as a repeatable test playbook.
Surfaced and fixed three real issues while building the E2E:
1. JWT secret mismatch — docker-compose.override.yml backend was using
a NoteLett-only JWT_SECRET that platform-service did not share, so
every Authorization: Bearer call returned 'Invalid or expired token'.
Aligned the override to use platform-service's actual secret
(dev-ecosystem-secret-do-not-use-in-production).
2. CORS preflight missing PATCH/DELETE — @bytelyst/fastify-core registers
@fastify/cors with only { origin }, which leaves Access-Control-Allow-
Methods at the @fastify/cors default of 'GET,HEAD,POST'. Real browser
PATCH/DELETE preflights would fail. Added an onSend hook in
backend/src/server.ts that rewrites the header to
'GET,HEAD,POST,PATCH,PUT,DELETE,OPTIONS' on CORS preflight responses.
3. Product 'notelett' wasn't registered with platform-service — auth
register/login both error with 'Unknown or disabled product: notelett'.
The seed script now POSTs to /api/products idempotently.
Deliverables:
- scripts/e2e-docker-seed.sh — idempotent: registers the notelett product
and creates two test users (admin@notelett.app with role=admin who can
write, user@notelett.app with role=user who is read-only). Re-runs are
no-ops once seeded.
- scripts/e2e-docker-test.sh — 9-step E2E that drives the deployed stack
via HTTP only (no browser): login → CORS preflight for PATCH →
workspace create → note create → note read → note PATCH (status:
draft→active) → note list → note delete → workspace delete.
- docs/testing/E2E_DOCKER_TESTING.md — full playbook covering prereqs,
seed, automated E2E, manual UI smoke, stack architecture diagram,
troubleshooting (JWT mismatch, unknown product, role rejection,
CORS, port conflict, data loss), tear-down, CI wiring guidance.
- package.json — pnpm e2e:docker:seed and pnpm e2e:docker:test
shortcuts.
Verified live on this host's deployed stack:
$ bash scripts/e2e-docker-seed.sh
↷ product 'notelett' already exists
↷ admin user already registered + login works
✓ user created
🟢 Seed complete.
$ bash scripts/e2e-docker-test.sh
✓ user=usr_e094e0c2-... role=admin
✓ CORS allows PATCH
✓ workspace created
✓ note created
✓ note read matches
✓ note patched (status: draft → active)
✓ note list returned (1 item)
✓ note deleted (HTTP 204)
✓ workspace deleted (HTTP 204)
🟢 All 9 E2E steps passed.
Backend regression suite still green: 380/380.
9.3 KiB
E2E Testing — Docker Compose Stack
Audience: anyone testing NoteLett locally via the Docker compose stack. Last verified: May 23, 2026 against
docker-compose.override.ymland platform-servicedev-ecosystem-secret-do-not-use-in-production.
What this exercises
The full critical path of a NoteLett user:
| Step | Surface | Endpoint / Action |
|---|---|---|
| 1 | platform-service (4003) | POST /api/auth/login — issues a JWT with iss=bytelyst-platform |
| 2 | notelett-backend (4016) | CORS preflight for PATCH from origin localhost:3050 |
| 3 | notelett-backend | POST /api/workspaces — requires admin role |
| 4 | notelett-backend | POST /api/notes |
| 5 | notelett-backend | GET /api/notes/:id?workspaceId=... (Cosmos point read) |
| 6 | notelett-backend | PATCH /api/notes/:id?workspaceId=... — flips status draft → active |
| 7 | notelett-backend | GET /api/notes?workspaceId=... (list) |
| 8 | notelett-backend | DELETE /api/notes/:id?workspaceId=... |
| 9 | notelett-backend | DELETE /api/workspaces/:id |
The backend itself does NOT issue tokens; it only consumes JWTs the platform-service signs. The two services share a JWT_SECRET via docker-compose.override.yml. If those don't match, every authenticated call returns Invalid or expired token.
Prerequisites
-
Sibling services running on host:
platform-serviceon:4003extraction-serviceon:4005(optional for this E2E, used elsewhere)mcp-serveron:4007(optional)cosmos-emulatoron:8081(optional —DB_PROVIDER=memoryis the default for compose)
-
Docker images built:
bash scripts/docker-prep.sh docker compose build bash scripts/docker-prep.sh --restore -
Compose up:
docker compose up -dThis brings up
notelett-backend(port 4016) andnotelett-web(host port 3050 → container 3045) viadocker-compose.override.yml. -
Verify health:
curl -sS http://localhost:4016/health # → {"status":"ok",...} curl -sS http://localhost:3050/dashboard # → HTTP 200
One-time seed (idempotent)
bash scripts/e2e-docker-seed.sh
What it does:
| Resource | Endpoint | Skip if exists |
|---|---|---|
notelett product on platform-service |
POST /api/products |
Yes (checks GET /api/products/notelett) |
Admin user admin@notelett.app |
POST /api/auth/register (role=admin) |
Yes (probes login first) |
Read-only user user@notelett.app |
POST /api/auth/register (role=user) |
Yes (probes login first) |
Test credentials produced:
| Password | Role | Can write? | |
|---|---|---|---|
admin@notelett.app |
Notelett!Test#2026 |
admin |
✅ yes |
user@notelett.app |
Notelett!Test#2026 |
user |
❌ no (requireWriter rejects user role) |
Run the E2E test
bash scripts/e2e-docker-test.sh
Expected output (last verified May 23, 2026):
── 1. Login as admin@notelett.app ──
✓ user=usr_e094e0c2-ef52-4ad5-b7af-6a1cb6df700a role=admin
── 2. CORS preflight for PATCH ──
✓ CORS allows PATCH
── 3. Create workspace ──
id=ws-e2e-... name=E2E Workspace
✓ workspace created
── 4. Create note ──
id=note-e2e-... status=draft title=E2E Note
✓ note created
── 5. Read note ──
title=E2E Note tags=['e2e']
✓ note read matches
── 6. PATCH note (title + status → active) ──
title=E2E Note (edited) status=active
✓ note patched
── 7. List notes in workspace ──
total=1 items=['E2E Note (edited)']
✓ note list returned
── 8. DELETE note ──
delete HTTP 204
✓ note deleted
── 9. DELETE workspace ──
delete HTTP 204
✓ workspace deleted
🟢 All 9 E2E steps passed.
Manual UI test
After seed + compose up:
- Open http://localhost:3050/login in a browser.
- Sign in with
admin@notelett.app/Notelett!Test#2026. - You should be redirected to
/dashboard. - Use the sidebar to navigate to Workspaces → click Create Workspace → name it, save.
- Open the workspace → Create Note → title + body → save.
- From the note detail page:
- Edit the title inline (auto-saves)
- Add a tag
- Try Smart Actions → "Shorten" (uses mock LLM in this deploy)
- Click Share → "Copy as Text"
- Navigate to Settings → confirm the Profile card shows
admin@notelett.app. - Sign out — should land on
/login.
If any step fails see the troubleshooting section below.
Stack architecture (this deployment)
┌────────────────────────────┐
Browser ───▶ │ notelett-web :3050 │ (Next.js standalone in Docker)
│ serves SPA + assets │
└─────────┬──────────────────┘
│ SPA calls
▼
┌───────────────────────────────┐
│ platform-service :4003 │ (auth, products, flags)
│ signs JWT with iss= │
│ bytelyst-platform │
└─────────┬─────────────────────┘
│ POST /api/auth/login → {accessToken, user}
▼
Browser stores accessToken in localStorage
│
│ All NoteLett API calls send
│ Authorization: Bearer <accessToken>
▼
┌───────────────────────────────┐
│ notelett-backend :4016 │ (Fastify, DB_PROVIDER=memory)
│ verifies JWT with SAME │
│ JWT_SECRET as platform │
│ /api/workspaces, /api/notes, │
│ ... │
└───────────────────────────────┘
Troubleshooting
Invalid or expired token from notelett-backend
Cause: JWT signing secret on platform-service does not match NoteLett backend.
Fix:
docker exec mygh-platform-service-1 env | grep JWT_SECRET
# Copy that value into docker-compose.override.yml under services.backend.environment.JWT_SECRET
docker compose up -d --force-recreate backend
Unknown or disabled product: notelett
Cause: platform-service product cache doesn't have notelett.
Fix: re-run bash scripts/e2e-docker-seed.sh — step 1 will register the product. If it already says "exists" but the error persists, the platform-service may need a cache reload — restart it: docker restart mygh-platform-service-1.
Write access requires editor, admin, or owner role
Cause: logged-in user has user or viewer role, not admin.
Fix: use admin@notelett.app, not user@notelett.app. The platform-service register endpoint accepts roles admin | viewer | user while NoteLett's requireWriter accepts editor | admin | owner. Only admin works for both write operations and platform-service registration.
CORS preflight rejects PATCH/DELETE
Cause: rare — only happens if backend was deployed before commit <this-feature-commit>.
Fix: rebuild backend image: bash scripts/docker-prep.sh && docker compose build backend && bash scripts/docker-prep.sh --restore && docker compose up -d --force-recreate backend.
Port 3000 already in use
Cause: another container/service (Grafana, etc.) holds host port 3000.
Fix: this repo's docker-compose.override.yml already remaps web to port 3050. If you need a different port, edit the override file and re-run docker compose up -d --force-recreate web.
Data resets on container restart
Expected — DB_PROVIDER=memory. To persist, set Cosmos credentials in docker-compose.override.yml:
services:
backend:
environment:
DB_PROVIDER: cosmos
COSMOS_ENDPOINT: http://host.docker.internal:8081
COSMOS_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
COSMOS_DATABASE: notelett_local
Then docker compose up -d --force-recreate backend.
Tear-down
docker compose down # stop containers, keep volumes
docker compose down --volumes # stop and clear data
CI integration
scripts/e2e-docker-test.sh is suitable for a CI job that:
- Starts the compose stack (or relies on services as GitHub Actions services)
- Runs
e2e-docker-seed.shonce - Runs
e2e-docker-test.sh
The seed script is idempotent so reruns are safe. Currently the GitHub Actions release-flows Playwright job covers browser-level E2E; e2e-docker-test.sh covers the HTTP-API E2E and can be wired in as a separate job after the existing docker-build job.
Related docs
docs/PRODUCTION_READINESS_HANDOFF_ROADMAP.md— full production gatedocs/runbooks/SECRET_MANAGEMENT.md— how to rotate the shared JWT secretdocs/runbooks/MEK_ROTATION.md— field-encryption key rotation (when Cosmos is enabled)