From b71117e4ca067a2360c8e15d07b9735caf4ebcc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Tue, 24 Mar 2026 23:58:01 +0100 Subject: [PATCH] Add PostgreSQL + BRouter to CI E2E tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI now runs the full stack for E2E tests: - PostgreSQL as a service container + schema push via drizzle - BRouter JAR + Berlin segment (E10_N50) with caching - Both cached across runs (BRouter JAR + segment) Removed all skip/workaround logic from tests — all 20 E2E tests now run in CI with real services. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 71 +++++++++++++++++++++++++++++++++++++++- e2e/integration.test.ts | 48 ++------------------------- e2e/planner.test.ts | 12 ------- 3 files changed, 73 insertions(+), 58 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 253a052..6c07ab8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,22 @@ jobs: name: E2E Tests needs: build runs-on: ubuntu-latest + services: + postgres: + image: postgis/postgis:16-3.4 + env: + POSTGRES_USER: trails + POSTGRES_PASSWORD: trails + POSTGRES_DB: trails + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U trails" + --health-interval 5s + --health-timeout 5s + --health-retries 5 + env: + DATABASE_URL: postgres://trails:trails@localhost:5432/trails steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 @@ -75,8 +91,61 @@ jobs: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile + + - name: Push database schema + run: pnpm db:push + + - name: Build and cache BRouter + id: brouter-cache + uses: actions/cache@v4 + with: + path: /tmp/brouter + key: brouter-1.7.8 + + - name: Download BRouter + if: steps.brouter-cache.outputs.cache-hit != 'true' + run: | + mkdir -p /tmp/brouter + wget -q "https://github.com/abrensch/brouter/releases/download/v1.7.8/brouter-1.7.8.zip" -O /tmp/brouter/brouter.zip + cd /tmp/brouter && unzip -o brouter.zip && mv brouter-1.7.8/* . && rmdir brouter-1.7.8 && rm brouter.zip + + - name: Cache BRouter segment + id: segment-cache + uses: actions/cache@v4 + with: + path: /tmp/brouter-segments + key: brouter-segment-E10_N50 + + - name: Download Berlin segment + if: steps.segment-cache.outputs.cache-hit != 'true' + run: | + mkdir -p /tmp/brouter-segments + wget -q "https://brouter.de/brouter/segments4/E10_N50.rd5" -O /tmp/brouter-segments/E10_N50.rd5 + + - name: Start BRouter + run: | + cd /tmp/brouter + java -Xmx256M -Xms64M \ + -DmaxRunningTime=300 \ + -cp brouter-1.7.8-all.jar \ + btools.server.RouteServer \ + /tmp/brouter-segments profiles2 profiles2 \ + 17777 2 & + # Wait for BRouter to start + for i in $(seq 1 30); do + curl -sf http://localhost:17777/brouter?lonlats=13.4,52.5\|13.5,52.5\&profile=trekking\&format=geojson > /dev/null 2>&1 && break + sleep 2 + done + env: + BROUTER_URL: http://localhost:17777 + - run: pnpm exec playwright install --with-deps chromium - - run: pnpm test:e2e + + - name: Run E2E tests + run: pnpm test:e2e + env: + BROUTER_URL: http://localhost:17777 + - uses: actions/upload-artifact@v7 if: ${{ !cancelled() }} with: diff --git a/e2e/integration.test.ts b/e2e/integration.test.ts index 3cd00bc..54c904b 100644 --- a/e2e/integration.test.ts +++ b/e2e/integration.test.ts @@ -2,38 +2,17 @@ import { test, expect } from "@playwright/test"; /** * Integration tests that require the full dev stack: - * - PostgreSQL (for auth and routes) + * - PostgreSQL (for sessions) * - BRouter (for route computation) * - * Run with: pnpm dev:full (in another terminal), then pnpm test:e2e - * These tests are skipped in CI unless services are available. + * In CI, these services are started by the workflow. + * Locally, run `pnpm dev:full` first. */ -const JOURNAL = "http://localhost:3000"; const PLANNER = "http://localhost:3001"; -// Helper: check if DB is available (checks Planner API) -async function isDbAvailable(): Promise { - try { - const resp = await fetch(`${PLANNER}/api/sessions`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({}), - }); - return resp.ok; // 201 = DB available, 503 = DB unavailable - } catch { - return false; - } -} - test.describe("Integration: Journal ↔ Planner handoff", () => { - test.beforeAll(async () => { - const dbAvailable = await isDbAvailable(); - test.skip(!dbAvailable, "Database not available — run pnpm dev:full"); - }); - test("GPX import → view route → export GPX", async ({ request }) => { - // This tests the API flow without needing WebAuthn const gpx = ` Berlin @@ -45,7 +24,6 @@ test.describe("Integration: Journal ↔ Planner handoff", () => { `; - // Create a planner session with GPX const sessionResp = await request.post(`${PLANNER}/api/sessions`, { data: { gpx }, }); @@ -57,25 +35,6 @@ test.describe("Integration: Journal ↔ Planner handoff", () => { }); test.describe("Integration: BRouter routing", () => { - test.beforeAll(async () => { - try { - const resp = await fetch(`${PLANNER}/api/route`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - waypoints: [ - { lat: 52.516, lon: 13.377 }, - { lat: 52.515, lon: 13.351 }, - ], - profile: "trekking", - }), - }); - test.skip(!resp.ok, "BRouter not available — start with pnpm dev:full"); - } catch { - test.skip(true, "BRouter not available"); - } - }); - test("computes route between Berlin waypoints", async ({ request }) => { const response = await request.post(`${PLANNER}/api/route`, { data: { @@ -108,7 +67,6 @@ test.describe("Integration: BRouter routing", () => { const geojson = await response.json(); const coords = geojson.features[0].geometry.coordinates; - // Route should pass near the middle waypoint (52.516, 13.377) const nearMiddle = coords.some( (c: number[]) => Math.abs(c[1] - 52.516) < 0.005 && Math.abs(c[0] - 13.377) < 0.005, diff --git a/e2e/planner.test.ts b/e2e/planner.test.ts index d3d6751..0d7d67c 100644 --- a/e2e/planner.test.ts +++ b/e2e/planner.test.ts @@ -6,18 +6,6 @@ test.describe("Planner", () => { await expect(page).toHaveTitle("trails.cool Planner"); await expect(page.getByText("Collaborative route planning")).toBeVisible(); }); -}); - -// Tests that require PostgreSQL — skip in CI -test.describe("Planner (requires DB)", () => { - test.beforeEach(async ({ request }) => { - try { - const resp = await request.post("/api/sessions", { data: {} }); - if (!resp.ok()) test.skip(); - } catch { - test.skip(); - } - }); test("can create a session via API", async ({ request }) => { const response = await request.post("/api/sessions", { data: {} });