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

225 lines
9.3 KiB
Markdown

# 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 <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:
```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 `<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`:
```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)