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.
111 lines
4.8 KiB
Bash
Executable File
111 lines
4.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# NoteLett Docker compose E2E test.
|
|
#
|
|
# Drives the full critical-path auth + notes flow against the deployed
|
|
# Docker stack via HTTP:
|
|
# 1. Login to platform-service (port 4003)
|
|
# 2. CORS preflight for PATCH (port 4016)
|
|
# 3. Create workspace (port 4016, requires admin role)
|
|
# 4. Create note (port 4016)
|
|
# 5. Read note by id (point read, /workspaceId partition)
|
|
# 6. PATCH note (title + status)
|
|
# 7. List notes in workspace
|
|
# 8. DELETE note
|
|
# 9. DELETE workspace
|
|
#
|
|
# Prerequisites:
|
|
# - `docker compose up -d` has been run from the repo root.
|
|
# - The sibling platform-service is running on http://localhost:4003 and
|
|
# has been seeded with the `notelett` product + a test admin user.
|
|
# Run scripts/e2e-docker-seed.sh once to do both.
|
|
# - JWT_SECRET in docker-compose.override.yml MUST match platform-service.
|
|
#
|
|
# Usage:
|
|
# bash scripts/e2e-docker-test.sh
|
|
#
|
|
# Environment overrides:
|
|
# PLATFORM default http://localhost:4003
|
|
# BACKEND default http://localhost:4016
|
|
# EMAIL default admin@notelett.app
|
|
# PASS default Notelett!Test#2026 (matches scripts/e2e-docker-seed.sh)
|
|
|
|
set -e
|
|
|
|
PLATFORM="${PLATFORM:-http://localhost:4003}"
|
|
BACKEND="${BACKEND:-http://localhost:4016}"
|
|
EMAIL="${EMAIL:-admin@notelett.app}"
|
|
PASS="${PASS:-Notelett!Test#2026}"
|
|
|
|
step() { echo ""; echo "── $* ──"; }
|
|
ok() { echo "✓ $*"; }
|
|
fail() { echo "✗ $*" >&2; exit 1; }
|
|
|
|
step "1. Login as $EMAIL"
|
|
LOGIN=$(curl -sS -X POST "$PLATFORM/api/auth/login" -H "Content-Type: application/json" \
|
|
-d "{\"email\":\"$EMAIL\",\"password\":\"$PASS\",\"productId\":\"notelett\"}")
|
|
TOKEN=$(echo "$LOGIN" | python3 -c "import json,sys; print(json.load(sys.stdin).get('accessToken',''))")
|
|
USER_ID=$(echo "$LOGIN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('user',{}).get('id',''))")
|
|
ROLE=$(echo "$LOGIN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('user',{}).get('role',''))")
|
|
if [[ -z "$TOKEN" ]]; then
|
|
echo "$LOGIN" >&2
|
|
fail "login failed — run scripts/e2e-docker-seed.sh first"
|
|
fi
|
|
ok "user=$USER_ID role=$ROLE"
|
|
|
|
step "2. CORS preflight for PATCH"
|
|
curl -sS -X OPTIONS "$BACKEND/api/notes/x" \
|
|
-H "Origin: http://localhost:3050" \
|
|
-H "Access-Control-Request-Method: PATCH" -D - -o /dev/null \
|
|
| grep -i "access-control-allow-methods" \
|
|
| grep -qE "PATCH" \
|
|
&& ok "CORS allows PATCH" \
|
|
|| fail "CORS does not advertise PATCH"
|
|
|
|
step "3. Create workspace"
|
|
WS_ID="ws-e2e-$(date +%s)"
|
|
WS=$(curl -sS -X POST "$BACKEND/api/workspaces" \
|
|
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
-d "{\"id\":\"$WS_ID\",\"name\":\"E2E Workspace\",\"description\":\"Created by e2e-docker-test.sh\"}")
|
|
echo "$WS" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f' id={d[\"id\"]} name={d[\"name\"]}')"
|
|
ok "workspace created"
|
|
|
|
step "4. Create note"
|
|
NOTE_ID="note-e2e-$(date +%s)"
|
|
NOTE=$(curl -sS -X POST "$BACKEND/api/notes" \
|
|
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
-d "{\"id\":\"$NOTE_ID\",\"workspaceId\":\"$WS_ID\",\"title\":\"E2E Note\",\"body\":\"<p>created by script</p>\",\"tags\":[\"e2e\"],\"sourceType\":\"manual\"}")
|
|
echo "$NOTE" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f' id={d[\"id\"]} status={d[\"status\"]} title={d[\"title\"]}')"
|
|
ok "note created"
|
|
|
|
step "5. Read note"
|
|
GOT=$(curl -sS "$BACKEND/api/notes/$NOTE_ID?workspaceId=$WS_ID" -H "Authorization: Bearer $TOKEN")
|
|
echo "$GOT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f' title={d[\"title\"]} tags={d[\"tags\"]}')"
|
|
[[ "$(echo "$GOT" | python3 -c "import json,sys; print(json.load(sys.stdin)[\"id\"])")" == "$NOTE_ID" ]] \
|
|
&& ok "note read matches" \
|
|
|| fail "note read mismatch"
|
|
|
|
step "6. PATCH note (title + status → active)"
|
|
PATCHED=$(curl -sS -X PATCH "$BACKEND/api/notes/$NOTE_ID?workspaceId=$WS_ID" \
|
|
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
-d '{"title":"E2E Note (edited)","status":"active"}')
|
|
echo "$PATCHED" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f' title={d[\"title\"]} status={d[\"status\"]}')"
|
|
ok "note patched"
|
|
|
|
step "7. List notes in workspace"
|
|
LIST=$(curl -sS "$BACKEND/api/notes?workspaceId=$WS_ID" -H "Authorization: Bearer $TOKEN")
|
|
echo "$LIST" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f' total={d[\"total\"]} items={[n[\"title\"] for n in d[\"items\"]]}')"
|
|
ok "note list returned"
|
|
|
|
step "8. DELETE note"
|
|
curl -sS -X DELETE "$BACKEND/api/notes/$NOTE_ID?workspaceId=$WS_ID" \
|
|
-H "Authorization: Bearer $TOKEN" -o /dev/null -w " delete HTTP %{http_code}\n"
|
|
ok "note deleted"
|
|
|
|
step "9. DELETE workspace"
|
|
curl -sS -X DELETE "$BACKEND/api/workspaces/$WS_ID" \
|
|
-H "Authorization: Bearer $TOKEN" -o /dev/null -w " delete HTTP %{http_code}\n"
|
|
ok "workspace deleted"
|
|
|
|
echo ""
|
|
echo "🟢 All 9 E2E steps passed."
|