learning_ai_notes/docs/testing/E2E_DOCKER_TESTING.md
saravanakumardb1 d5e857dbf7 test(e2e): docker compose E2E test + seed scripts + 9-step verification
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.
2026-05-23 01:16:19 -07:00

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.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 scripts/docker-prep.sh
    docker compose build
    bash scripts/docker-prep.sh --restore
    
  3. Compose up:

    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:

    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:

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 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 <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:

  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.