# ABOUTME: E2E test fixtures and server harness for Playwright tests. # ABOUTME: Provides a live server instance for browser-based testing. import os import random import subprocess import sys import time import pytest import requests from animaltrack.db import get_db from animaltrack.migrations import run_migrations from animaltrack.seeds import run_seeds class ServerHarness: """Manages a live AnimalTrack server for e2e tests. Starts the server as a subprocess with an isolated test database, waits for it to be ready, and cleans up after tests complete. """ def __init__(self, port: int): self.port = port self.url = f"http://127.0.0.1:{port}" self.process = None def start(self, db_path: str): """Start the server with the given database.""" env = { **os.environ, "DB_PATH": db_path, "DEV_MODE": "true", "CSRF_SECRET": "e2e-test-csrf-secret-32chars!!", "TRUSTED_PROXY_IPS": "127.0.0.1", } # Use sys.executable to ensure we use the same Python environment self.process = subprocess.Popen( [ sys.executable, "-m", "animaltrack.cli", "serve", "--port", str(self.port), "--host", "127.0.0.1", ], env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) self._wait_for_ready() def _wait_for_ready(self, timeout: float = 30.0): """Poll /healthz until server is ready.""" start = time.time() while time.time() - start < timeout: try: response = requests.get(f"{self.url}/healthz", timeout=1) if response.ok: return except requests.RequestException: pass time.sleep(0.1) # If we get here, dump stderr for debugging if self.process: stderr = self.process.stderr.read() if self.process.stderr else b"" raise TimeoutError( f"Server not ready after {timeout}s. stderr: {stderr.decode('utf-8', errors='replace')}" ) def stop(self): """Stop the server and clean up.""" if self.process: self.process.terminate() try: self.process.wait(timeout=5) except subprocess.TimeoutExpired: self.process.kill() self.process.wait() @pytest.fixture(scope="session") def e2e_db_path(tmp_path_factory): """Create and migrate a fresh database for e2e tests. Session-scoped so all e2e tests share the same database state. """ temp_dir = tmp_path_factory.mktemp("e2e") db_path = str(temp_dir / "animaltrack.db") # Run migrations run_migrations(db_path, "migrations", verbose=False) # Seed with test data db = get_db(db_path) run_seeds(db) return db_path @pytest.fixture(scope="session") def live_server(e2e_db_path): """Start the server for the entire e2e test session. Uses a random port in the 33660-33759 range to avoid conflicts with other services or parallel test runs. """ port = 33660 + random.randint(0, 99) harness = ServerHarness(port) harness.start(e2e_db_path) yield harness harness.stop() @pytest.fixture(scope="session") def base_url(live_server): """Provide the base URL for the live server.""" return live_server.url