diff --git a/.forgejo/scripts/e2e-run.sh b/.forgejo/scripts/e2e-run.sh new file mode 100755 index 0000000..cd2812c --- /dev/null +++ b/.forgejo/scripts/e2e-run.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Playwright-E2E (Projekte api, web, throttle) gegen den laufenden Supabase-Stack im dind (Issue #38, D2). +# Voraussetzungen: DOCKER_HOST/DOCKER_GW gesetzt, `supabase start` lief, Extra-GoTrue auf :9998 laeuft, +# Web-Build in app/build/web. Die Supabase-Ports liegen im dind; ein TCP-Relay macht sie unter +# 127.0.0.1 erreichbar, wie es die Tests und die Web-App (CORS/Origin, Site-URL localhost:3000) erwarten. +# Der Spike (SPIKE_GOTRUE_URL) laeuft in CI bewusst nicht. +set -euo pipefail + +: "${DOCKER_GW:?DOCKER_GW (dind-Gateway) fehlt}" +: "${SUPABASE_ANON_KEY:?SUPABASE_ANON_KEY fehlt}" + +node e2e/support/tcp-forward.mjs "$DOCKER_GW" 54321 54322 54324 9998 & +relay=$! +trap 'kill "$relay" 2>/dev/null || true' EXIT + +for port in 54321 54322 54324 9998; do + for _ in $(seq 1 20); do + (exec 3<>"/dev/tcp/127.0.0.1/${port}") 2>/dev/null && break + sleep 0.5 + done +done + +export SUPABASE_URL=http://127.0.0.1:54321 +export MAILPIT_URL=http://127.0.0.1:54324 +export E2E_DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres +export WEB_URL=http://localhost:3000 +export THROTTLE_GOTRUE_URL=http://127.0.0.1:9998 +export CI=true + +cd e2e +npx playwright test --project=api --project=web --project=throttle diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index a480a10..0bf098e 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -182,3 +182,132 @@ jobs: - name: Aufräumen if: always() run: npx --yes supabase@${{ env.SUPABASE_CLI_VERSION }} stop --no-backup || true + + # Web-E2E (Playwright: api, web, throttle) gegen lokalen Supabase-Stack und gebaute Flutter-Web-App (Issue #38, Spec #1 D2). + # Läuft wie supabase-tests auf "docker-host" (Docker-CLI + dind per TCP). Der Stack liegt im dind; ein + # TCP-Relay (e2e/support/tcp-forward.mjs) macht ihn im Job unter 127.0.0.1 erreichbar. + # SICHERHEIT: Der Job führt PR-Code mit Zugriff auf den dind-Daemon des Runners aus (Daemon = Root-Äquivalent). + # Deshalb nicht für Fork-PRs (Bedingung unten); Fork-Beiträge nur nach Review und Übernahme in einen Branch dieses Repos. + # Gleiches Risiko hat supabase-tests; dort ist es (noch) nicht eingeschränkt. + # needs: supabase-tests, weil beide Jobs dieselben festen Container-Namen/Ports (project_id shuffle, 54321 ...) im dind belegen. + # Der Spike (SPIKE_GOTRUE_URL, Issue #37) läuft nicht in CI. + e2e: + runs-on: docker-host + needs: [supabase-tests] + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + env: + DOCKER_CLI_VERSION: 27.3.1 + DIND_PORT: 2375 + PUB_CACHE: ${{ github.workspace }}/.pub-cache + PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers + steps: + - uses: https://code.forgejo.org/actions/checkout@v4 + + - name: Werkzeuge prüfen + run: | + missing="" + for t in git curl unzip xz jq; do command -v "$t" >/dev/null || missing="$missing $t"; done + if [ -n "$missing" ]; then + apt-get update -qq && apt-get install -y -qq --no-install-recommends $missing xz-utils ca-certificates + fi + node --version + + - name: Docker-CLI installieren und dind anbinden + run: | + arch=$(uname -m) + curl -fsSL "https://download.docker.com/linux/static/stable/${arch}/docker-${DOCKER_CLI_VERSION}.tgz" \ + | tar -xz -C /usr/local/bin --strip-components=1 docker/docker + hex=$(awk '$2=="00000000"{print $3; exit}' /proc/net/route) + gw=$(printf '%d.%d.%d.%d' "0x${hex:6:2}" "0x${hex:4:2}" "0x${hex:2:2}" "0x${hex:0:2}") + echo "DOCKER_GW=${gw}" >> "$GITHUB_ENV" + echo "DOCKER_HOST=tcp://${gw}:${DIND_PORT}" >> "$GITHUB_ENV" + DOCKER_HOST="tcp://${gw}:${DIND_PORT}" docker version --format 'dind {{.Server.Version}}' + + - name: Supabase starten (mit Mailpit, ohne Studio u. a.) + run: npx --yes supabase@${{ env.SUPABASE_CLI_VERSION }} start -x studio,imgproxy,edge-runtime,logflare,vector + + - name: Datenbank zurücksetzen + run: npx --yes supabase@${{ env.SUPABASE_CLI_VERSION }} db reset + + - name: Lokale Schlüssel lesen + # Die Werte sind die öffentlichen Demo-Schlüssel der CLI, trotzdem nur maskiert weiterreichen. + run: | + status=$(npx --yes supabase@${{ env.SUPABASE_CLI_VERSION }} status -o env) + get() { printf '%s\n' "$status" | sed -n "s/^$1=\"\{0,1\}\([^\"]*\)\"\{0,1\}$/\1/p" | head -n1; } + anon=$(get ANON_KEY); service=$(get SERVICE_ROLE_KEY) + test -n "$anon" && test -n "$service" || { echo "ANON_KEY/SERVICE_ROLE_KEY nicht in 'supabase status -o env' gefunden"; exit 1; } + echo "::add-mask::$anon" + echo "::add-mask::$service" + echo "SUPABASE_ANON_KEY=${anon}" >> "$GITHUB_ENV" + echo "SUPABASE_SERVICE_ROLE_KEY=${service}" >> "$GITHUB_ENV" + # Env-Datei für den Web-Build: Platzhalter-Datei bleibt unverändert, der Schlüssel kommt zur Laufzeit. + jq --arg k "$anon" '.SUPABASE_ANON_KEY = $k | .SUPABASE_URL = "http://127.0.0.1:54321"' \ + app/env/ci.example.json > "${RUNNER_TEMP:-/tmp}/ci.e2e.json" + echo "E2E_ENV_FILE=${RUNNER_TEMP:-/tmp}/ci.e2e.json" >> "$GITHUB_ENV" + + - name: Extra-GoTrue für Drosselung (Krit. 7) starten + run: | + BIND_ADDR=0.0.0.0 HEALTH_HOST="$DOCKER_GW" bash e2e/support/start-throttle-gotrue.sh start throttle + + - name: Flutter-SDK-Cache + uses: https://code.forgejo.org/actions/cache@v4 + with: + path: ${{ env.FLUTTER_HOME }} + key: flutter-${{ env.FLUTTER_VERSION }}-docker-host + + - name: Flutter installieren + run: | + if [ ! -x "$FLUTTER_HOME/bin/flutter" ]; then + rm -rf "$FLUTTER_HOME" + git clone --depth 1 --branch "$FLUTTER_VERSION" https://github.com/flutter/flutter.git "$FLUTTER_HOME" + fi + echo "$FLUTTER_HOME/bin" >> "$GITHUB_PATH" + export PATH="$FLUTTER_HOME/bin:$PATH" + export CI=true + flutter config --no-analytics --enable-web + flutter precache --web + + - name: Pub-Cache + uses: https://code.forgejo.org/actions/cache@v4 + with: + path: ${{ github.workspace }}/.pub-cache + key: pub-${{ env.FLUTTER_VERSION }}-${{ hashFiles('app/pubspec.lock') }} + restore-keys: pub-${{ env.FLUTTER_VERSION }}- + + - name: Web-Build mit lokalem Anon-Key + working-directory: app + run: | + flutter pub get + flutter build web --release --dart-define-from-file="$E2E_ENV_FILE" + + - name: Playwright-Cache + uses: https://code.forgejo.org/actions/cache@v4 + with: + path: ${{ github.workspace }}/.playwright-browsers + key: playwright-${{ hashFiles('e2e/package-lock.json') }} + + - name: E2E-Abhängigkeiten und Browser + working-directory: e2e + run: | + npm ci + npx playwright install --with-deps chromium + + - name: Playwright (api, web, throttle) + run: bash .forgejo/scripts/e2e-run.sh + + - name: Artefakte hochladen (Report, Traces) + if: failure() + uses: https://code.forgejo.org/forgejo/upload-artifact@v4 + with: + name: playwright-${{ github.run_number }} + path: | + e2e/playwright-report + e2e/test-results + retention-days: 7 + if-no-files-found: ignore + + - name: Aufräumen + if: always() + run: | + bash e2e/support/start-throttle-gotrue.sh stop throttle || true + npx --yes supabase@${{ env.SUPABASE_CLI_VERSION }} stop --no-backup || true diff --git a/docs/auth-konfiguration.md b/docs/auth-konfiguration.md index 9eeeddc..c3b2c5c 100644 --- a/docs/auth-konfiguration.md +++ b/docs/auth-konfiguration.md @@ -190,6 +190,18 @@ Die Playwright-Tests unter `e2e/` (Projekte `api`, `web`, `throttle`, `spike`; A | AUTH-CFG-14, -15, -16 | `krit05-sitzung.spec.ts`, `config.spec.ts` | | AUTH-CFG-17, -18, -19 | `krit11-sprache-benachrichtigungen.spec.ts` | +### CI-Job `e2e` (Issue #38, Spec #1 D2) + +Der Job `e2e` in `.forgejo/workflows/ci.yml` läuft auf dem Runner `docker-host` (Docker-CLI + dind per TCP, wie `supabase-tests`) und führt die Projekte `api`, `web` und `throttle` aus. Ablauf: + +1. `supabase start -x studio,imgproxy,edge-runtime,logflare,vector` (Mailpit und Postgres bleiben) und `supabase db reset`. +2. Anon- und Service-Role-Key kommen zur Laufzeit aus `supabase status -o env`; daraus entsteht eine temporäre Env-Datei (`jq` auf `app/env/ci.example.json`, im `RUNNER_TEMP`, nicht im Repo) für `flutter build web --dart-define-from-file`. Die Datei `ci.example.json` bleibt unverändert. +3. Zweiter GoTrue-Container für Krit. 7: `BIND_ADDR=0.0.0.0 HEALTH_HOST= e2e/support/start-throttle-gotrue.sh start throttle` (Port 9998). +4. `.forgejo/scripts/e2e-run.sh` startet den TCP-Relay `e2e/support/tcp-forward.mjs` (127.0.0.1:54321/54322/54324/9998 -> dind-Gateway) und ruft `npx playwright test --project=api --project=web --project=throttle` auf. Der Relay ist nötig, weil die Ports im dind liegen, die Tests, die Web-App (CORS/Origin) und `e2e/support/db.ts` (erlaubt nur lokale Hosts) aber 127.0.0.1 erwarten. Die statische Auslieferung der Web-App auf Port 3000 übernimmt das `webServer` der Playwright-Konfiguration (`serve -s`). +5. Bei Fehlern werden `e2e/playwright-report` und `e2e/test-results` (Traces) als Artefakt `playwright-` hochgeladen. Der Schritt „Aufräumen“ (`if: always()`) entfernt den Extra-Container und stoppt den Supabase-Stack. + +Das Projekt `spike` (SMTP-Ausfall, `SPIKE_GOTRUE_URL`) läuft in CI nicht. Der Job hat `needs: supabase-tests` (gleiche Container-Namen und Ports im dind) und läuft nicht für Fork-PRs: Wer PR-Code auf `docker-host` ausführt, hat Zugriff auf den dind-Daemon des Runners. + ### SMTP-Ausfall (Spike #37, Stand 2026-11-03, GoTrue `v2.197.0`) Messung gegen einen zusätzlichen GoTrue-Container, dessen SMTP-Port 1 die Verbindung ablehnt (`e2e/support/start-throttle-gotrue.sh start smtp-down`, Test `e2e/tests/spike/smtp-down.spec.ts`). Mailpit bleibt für die anderen Tests unberührt. Die Tests prüfen nichts, sie protokollieren. diff --git a/e2e/support/start-throttle-gotrue.sh b/e2e/support/start-throttle-gotrue.sh index 4cb4d88..963e0dd 100755 --- a/e2e/support/start-throttle-gotrue.sh +++ b/e2e/support/start-throttle-gotrue.sh @@ -14,9 +14,14 @@ # Der Container nutzt dasselbe Image, dieselbe DB und dasselbe Docker-Netz wie der Auth-Container # der CLI; die Umgebung wird per `docker inspect` uebernommen (Version nicht hart kodiert). Die # Rate-Limit-Buckets liegen im Speicher und sind damit von der CLI getrennt (R13). +# Docker-in-Docker (CI, DOCKER_HOST=tcp://...): Der Port wird im dind veroeffentlicht, nicht im Job-Container. +# Dann BIND_ADDR=0.0.0.0 und HEALTH_HOST= setzen (siehe .forgejo/workflows/ci.yml, Job e2e); +# die Tests erreichen den Container ueber e2e/support/tcp-forward.mjs auf 127.0.0.1. # Nur lokal/CI benutzen: Die kopierte Umgebung enthaelt die lokalen Demo-Secrets der CLI. set -euo pipefail +BIND_ADDR="${BIND_ADDR:-127.0.0.1}" +HEALTH_HOST="${HEALTH_HOST:-127.0.0.1}" ACTION="${1:-start}" PROFILE="${2:-throttle}" SOURCE="${SOURCE_CONTAINER:-$(docker ps --format '{{.Names}}' | grep -E '^supabase_auth_' | head -n1 || true)}" @@ -57,12 +62,12 @@ case "$PROFILE" in smtp-down) ENV_ARGS+=(-e "GOTRUE_SMTP_PORT=1") ;; # Host bleibt Mailpit, Port 1 lehnt die Verbindung ab esac -docker run -d --name "$NAME" --network "$NETWORK" -p "127.0.0.1:${HOST_PORT}:9999" \ +docker run -d --name "$NAME" --network "$NETWORK" -p "${BIND_ADDR}:${HOST_PORT}:9999" \ "${ENV_ARGS[@]}" "$IMAGE" auth >/dev/null for _ in $(seq 1 40); do - if curl -fsS "http://127.0.0.1:${HOST_PORT}/health" >/dev/null 2>&1; then - echo "Container $NAME ($IMAGE, Profil $PROFILE) laeuft auf http://127.0.0.1:${HOST_PORT}" + if curl -fsS "http://${HEALTH_HOST}:${HOST_PORT}/health" >/dev/null 2>&1; then + echo "Container $NAME ($IMAGE, Profil $PROFILE) laeuft auf http://${HEALTH_HOST}:${HOST_PORT}" exit 0 fi sleep 0.5 diff --git a/e2e/support/tcp-forward.mjs b/e2e/support/tcp-forward.mjs new file mode 100644 index 0000000..c937bba --- /dev/null +++ b/e2e/support/tcp-forward.mjs @@ -0,0 +1,40 @@ +// Einfacher TCP-Relay fuer CI unter Docker-in-Docker (Issue #38, D2). +// +// Die Supabase-Ports (Kong 54321, Postgres 54322, Mailpit 54324, Extra-GoTrue 9998) liegen im dind, +// nicht im Job-Container. Die Tests und die Web-App erwarten 127.0.0.1. Dieses Skript leitet +// 127.0.0.1: an : weiter (HTTP und Postgres, rein TCP). +// +// Aufruf: node tcp-forward.mjs [ ...] +import net from 'node:net'; + +const [host, ...ports] = process.argv.slice(2); +if (!host || ports.length === 0) { + console.error('Aufruf: node tcp-forward.mjs [ ...]'); + process.exit(2); +} + +for (const p of ports) { + const port = Number(p); + if (!Number.isInteger(port) || port < 1 || port > 65535) { + console.error(`Ungueltiger Port: ${p}`); + process.exit(2); + } + net + .createServer((client) => { + const upstream = net.connect(port, host); + client.pipe(upstream).pipe(client); + const close = () => { + client.destroy(); + upstream.destroy(); + }; + client.on('error', close); + upstream.on('error', close); + client.on('close', close); + upstream.on('close', close); + }) + .on('error', (err) => { + console.error(`Relay ${port}: ${err.message}`); + process.exit(1); + }) + .listen(port, '127.0.0.1', () => console.log(`127.0.0.1:${port} -> ${host}:${port}`)); +} diff --git a/supabase/README.md b/supabase/README.md index d25fab8..b805cd6 100644 --- a/supabase/README.md +++ b/supabase/README.md @@ -26,6 +26,7 @@ supabase test db # alle pgTAP-Tests ausführen - `e2e/` testet GoTrue über Kong (`api`), die gebaute Web-App (`web`), die IP-Drosselung (`throttle`) und den SMTP-Ausfall (`spike`). Voraussetzung: `supabase start` mit Mailpit und Postgres (Ports 54321, 54322, 54324). - `e2e/support/db.ts` verbindet sich direkt mit dem lokalen Postgres (Rolle `postgres`) und setzt `recovery_sent_at`/`confirmation_sent_at` für die Ablauftests; es bricht bei nicht-lokalem Host ab. - Der Extra-Container für `throttle`/`spike` startet mit `e2e/support/start-throttle-gotrue.sh`. Details: [`e2e/README.md`](../e2e/README.md). +- CI: Job `e2e` in `.forgejo/workflows/ci.yml` (Runner `docker-host`, dind); Ablauf in [`docs/auth-konfiguration.md`](../docs/auth-konfiguration.md) Abschnitt 7. ## Regeln - Jede Tabelle hat `id` (uuid, PK), `created_at`, `updated_at` und Row Level Security.