124 lines
3.6 KiB
Python
124 lines
3.6 KiB
Python
import os
|
|
import subprocess
|
|
import time
|
|
from typing import Dict, Generator
|
|
|
|
import pytest
|
|
# type: ignore[import]
|
|
from playwright.sync_api import Browser, Page, Playwright, sync_playwright
|
|
|
|
import httpx
|
|
from sqlalchemy.engine import make_url
|
|
|
|
# Use a different port for the test server to avoid conflicts
|
|
TEST_PORT = 8001
|
|
BASE_URL = f"http://localhost:{TEST_PORT}"
|
|
|
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
|
def live_server() -> Generator[str, None, None]:
|
|
"""Launch a live test server in a separate process."""
|
|
env = _prepare_database_environment(os.environ.copy())
|
|
|
|
process = subprocess.Popen(
|
|
[
|
|
"uvicorn",
|
|
"main:app",
|
|
"--host",
|
|
"127.0.0.1",
|
|
f"--port={TEST_PORT}",
|
|
],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
env=env,
|
|
)
|
|
|
|
deadline = time.perf_counter() + 30
|
|
last_error: Exception | None = None
|
|
while time.perf_counter() < deadline:
|
|
if process.poll() is not None:
|
|
raise RuntimeError("uvicorn server exited before becoming ready")
|
|
try:
|
|
response = httpx.get(BASE_URL, timeout=1.0)
|
|
if response.status_code < 500:
|
|
break
|
|
except Exception as exc: # noqa: BLE001
|
|
last_error = exc
|
|
time.sleep(0.5)
|
|
else:
|
|
process.terminate()
|
|
process.wait(timeout=5)
|
|
raise TimeoutError(
|
|
"Timed out waiting for uvicorn test server to start"
|
|
) from last_error
|
|
|
|
try:
|
|
yield BASE_URL
|
|
finally:
|
|
if process.poll() is None:
|
|
process.terminate()
|
|
try:
|
|
process.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
process.wait(timeout=5)
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def playwright_instance() -> Generator[Playwright, None, None]:
|
|
"""Provide a Playwright instance for the test session."""
|
|
with sync_playwright() as p:
|
|
yield p
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def browser(
|
|
playwright_instance: Playwright,
|
|
) -> Generator[Browser, None, None]:
|
|
"""Provide a browser instance for the test session."""
|
|
browser = playwright_instance.chromium.launch()
|
|
yield browser
|
|
browser.close()
|
|
|
|
|
|
@pytest.fixture()
|
|
def page(browser: Browser, live_server: str) -> Generator[Page, None, None]:
|
|
"""Provide a new page for each test."""
|
|
page = browser.new_page(base_url=live_server)
|
|
page.goto("/")
|
|
page.wait_for_load_state("networkidle")
|
|
yield page
|
|
page.close()
|
|
|
|
|
|
def _prepare_database_environment(env: Dict[str, str]) -> Dict[str, str]:
|
|
"""Ensure granular database env vars are available for the app under test."""
|
|
|
|
required = ("DATABASE_HOST", "DATABASE_USER", "DATABASE_NAME")
|
|
if all(env.get(key) for key in required):
|
|
return env
|
|
|
|
legacy_url = env.get("DATABASE_URL")
|
|
if not legacy_url:
|
|
return env
|
|
|
|
url = make_url(legacy_url)
|
|
env.setdefault("DATABASE_DRIVER", url.drivername)
|
|
if url.host:
|
|
env.setdefault("DATABASE_HOST", url.host)
|
|
if url.port:
|
|
env.setdefault("DATABASE_PORT", str(url.port))
|
|
if url.username:
|
|
env.setdefault("DATABASE_USER", url.username)
|
|
if url.password:
|
|
env.setdefault("DATABASE_PASSWORD", url.password)
|
|
if url.database:
|
|
env.setdefault("DATABASE_NAME", url.database)
|
|
|
|
query_options = dict(url.query) if url.query else {}
|
|
options = query_options.get("options")
|
|
if isinstance(options, str) and "search_path=" in options:
|
|
env.setdefault("DATABASE_SCHEMA", options.split("search_path=")[-1])
|
|
|
|
return env
|