""" Session-scoped fixtures that spin up and tear down the test stack. The test stack uses docker-compose.test.yml which: - Runs on port 18000 (won't collide with the dev stack on 8000) - Uses tmpfs for all storage (no data survives after `down`) - Uses project name "langlearn-test" to stay isolated from dev containers """ import pathlib import subprocess from dotenv import load_dotenv import uuid import httpx import pytest PROJECT_ROOT = pathlib.Path(__file__).parent.parent COMPOSE_FILE = str(PROJECT_ROOT / "docker-compose.test.yml") ENV_FILE = str(PROJECT_ROOT / ".env.test") COMPOSE_PROJECT = "langlearn-test" API_BASE_URL = "http://localhost:18000" load_dotenv(PROJECT_ROOT / ".env.test") def _compose(*args: str) -> None: subprocess.run( ["docker", "compose", "-p", COMPOSE_PROJECT, "-f", COMPOSE_FILE, *args], cwd=PROJECT_ROOT, check=True, ) @pytest.fixture(scope="session", autouse=True) def docker_stack(): """Bring the test stack up before the session; tear it down (including volumes) after.""" _compose("--env-file", ENV_FILE, "up", "--build", "--wait", "-d") yield _compose("down", "-v") @pytest.fixture def client() -> httpx.Client: """A plain httpx client pointed at the test API. Not authenticated.""" with httpx.Client(base_url=API_BASE_URL) as c: yield c def _random_email() -> str: return f"user-{uuid.uuid4()}@example.com" @pytest.fixture def authd_client() -> httpx.Client: email = _random_email() password = "password1234" with httpx.Client(base_url=API_BASE_URL) as client: register_response = client.post("/api/auth/register", json={"email": email, "password": password}) assert register_response.json().get("success") is True, f"Failed to register test user: {register_response.text}" login_response = client.post("/api/auth/login", json={"email": email, "password": password}) assert login_response.status_code == 200, f"Failed to log in test user: {login_response.text}" token = login_response.json().get("access_token") client.headers["Authorization"] = f"Bearer {token}" yield client