# E2E Testing — Docker Compose Stack > **Audience:** anyone testing NoteLett locally via the Docker compose stack. > **Last verified:** May 23, 2026 against `docker-compose.override.yml` and platform-service `dev-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 1. **Sibling services running** on host: - `platform-service` on `:4003` - `extraction-service` on `:4005` (optional for this E2E, used elsewhere) - `mcp-server` on `:4007` (optional) - `cosmos-emulator` on `:8081` (optional — `DB_PROVIDER=memory` is the default for compose) 2. **Docker images built**: ```bash bash scripts/docker-prep.sh docker compose build bash scripts/docker-prep.sh --restore ``` 3. **Compose up**: ```bash docker compose up -d ``` This brings up `notelett-backend` (port 4016) and `notelett-web` (host port 3050 → container 3045) via `docker-compose.override.yml`. 4. **Verify health**: ```bash curl -sS http://localhost:4016/health # → {"status":"ok",...} curl -sS http://localhost:3050/dashboard # → HTTP 200 ``` ## One-time seed (idempotent) ```bash 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: | Email | 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 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: 1. Open **http://localhost:3050/login** in a browser. 2. Sign in with `admin@notelett.app` / `Notelett!Test#2026`. 3. You should be redirected to `/dashboard`. 4. Use the sidebar to navigate to **Workspaces** → click **Create Workspace** → name it, save. 5. Open the workspace → **Create Note** → title + body → save. 6. 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" 7. Navigate to **Settings** → confirm the Profile card shows `admin@notelett.app`. 8. 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 ▼ ┌───────────────────────────────┐ │ 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: ```bash 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 ``. 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`: ```yaml 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 ```bash 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: 1. Starts the compose stack (or relies on services as GitHub Actions services) 2. Runs `e2e-docker-seed.sh` once 3. 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`](../PRODUCTION_READINESS_HANDOFF_ROADMAP.md) — full production gate - [`docs/runbooks/SECRET_MANAGEMENT.md`](../runbooks/SECRET_MANAGEMENT.md) — how to rotate the shared JWT secret - [`docs/runbooks/MEK_ROTATION.md`](../runbooks/MEK_ROTATION.md) — field-encryption key rotation (when Cosmos is enabled)