""" Conftest для E2E тестов Team Board API. Поднимает отдельный Tracker (Docker) на тестовой БД team_board_test:8101. После тестов — дропает БД и убивает контейнер. """ import pytest import pytest_asyncio import httpx import uuid import subprocess import time from typing import Dict, Any TEST_DB = "team_board_test" TEST_PORT = 8101 BASE_URL = f"http://localhost:{TEST_PORT}/api/v1" TRACKER_DIR = "/root/projects/team-board/tracker" def pytest_configure(config): """Создаём тестовую БД, запускаем Tracker в Docker.""" # 1. Создать тестовую БД subprocess.run( ["sudo", "-u", "postgres", "psql", "-c", f"DROP DATABASE IF EXISTS {TEST_DB};"], capture_output=True, ) result = subprocess.run( ["sudo", "-u", "postgres", "psql", "-c", f"CREATE DATABASE {TEST_DB} OWNER team_board;"], capture_output=True, ) if result.returncode != 0: raise RuntimeError(f"Failed to create test DB: {result.stderr.decode()}") # 2. Запустить Tracker-test контейнер subprocess.run( ["docker", "compose", "-f", "docker-compose.test.yml", "down", "-v"], cwd=TRACKER_DIR, capture_output=True, ) result = subprocess.run( ["docker", "compose", "-f", "docker-compose.test.yml", "up", "-d", "--build"], cwd=TRACKER_DIR, capture_output=True, ) if result.returncode != 0: raise RuntimeError(f"Failed to start test Tracker: {result.stderr.decode()}") # 3. Ждём пока ответит for i in range(30): try: r = httpx.get(f"http://localhost:{TEST_PORT}/api/v1/labels", timeout=2) if r.status_code in (200, 401): break except Exception: pass time.sleep(1) else: subprocess.run( ["docker", "compose", "-f", "docker-compose.test.yml", "logs"], cwd=TRACKER_DIR, ) raise RuntimeError("Test Tracker did not start in 30s") def pytest_unconfigure(config): """Убиваем контейнер и дропаем тестовую БД.""" subprocess.run( ["docker", "compose", "-f", "docker-compose.test.yml", "down", "-v"], cwd=TRACKER_DIR, capture_output=True, ) subprocess.run( ["sudo", "-u", "postgres", "psql", "-c", f"DROP DATABASE IF EXISTS {TEST_DB};"], capture_output=True, ) # ---------- Fixtures ---------- @pytest.fixture(scope="session") def base_url(): return BASE_URL @pytest_asyncio.fixture async def admin_token(base_url: str) -> str: async with httpx.AsyncClient() as client: response = await client.post(f"{base_url}/auth/login", json={ "login": "admin", "password": "teamboard" }) assert response.status_code == 200 return response.json()["token"] @pytest.fixture def agent_token(): return "tb-coder-dev-token" @pytest_asyncio.fixture async def http_client(base_url: str, admin_token: str): headers = {"Authorization": f"Bearer {admin_token}"} async with httpx.AsyncClient(base_url=base_url, headers=headers, timeout=30.0) as client: yield client @pytest_asyncio.fixture async def agent_client(base_url: str, agent_token: str): headers = {"Authorization": f"Bearer {agent_token}"} async with httpx.AsyncClient(base_url=base_url, headers=headers, timeout=30.0) as client: yield client @pytest_asyncio.fixture async def test_project(http_client: httpx.AsyncClient) -> Dict[str, Any]: slug = f"test-project-{uuid.uuid4().hex[:8]}" response = await http_client.post("/projects", json={ "name": f"Test Project {slug}", "slug": slug, "description": "E2E test", }) assert response.status_code == 200 project = response.json() yield project await http_client.delete(f"/projects/{project['id']}") @pytest_asyncio.fixture async def test_user(http_client: httpx.AsyncClient) -> Dict[str, Any]: slug = f"test-user-{uuid.uuid4().hex[:8]}" response = await http_client.post("/members", json={ "name": f"Test User {slug}", "slug": slug, "type": "human", "role": "member", }) assert response.status_code == 200 yield response.json() @pytest_asyncio.fixture async def test_agent(http_client: httpx.AsyncClient) -> Dict[str, Any]: slug = f"test-agent-{uuid.uuid4().hex[:8]}" response = await http_client.post("/members", json={ "name": f"Test Agent {slug}", "slug": slug, "type": "agent", "role": "member", "agent_config": {"capabilities": ["coding"], "labels": ["backend"], "chat_listen": "mentions", "task_listen": "assigned"}, }) assert response.status_code == 200 yield response.json() @pytest_asyncio.fixture async def test_task(http_client: httpx.AsyncClient, test_project: Dict[str, Any]) -> Dict[str, Any]: response = await http_client.post(f"/tasks?project_id={test_project['id']}", json={ "title": "Test Task", "description": "E2E test", "type": "task", "status": "backlog", "priority": "medium", }) assert response.status_code == 200 yield response.json() @pytest_asyncio.fixture async def test_label(http_client: httpx.AsyncClient) -> Dict[str, Any]: name = f"test-label-{uuid.uuid4().hex[:8]}" response = await http_client.post("/labels", json={"name": name, "color": "#ff5733"}) assert response.status_code == 200 label = response.json() yield label await http_client.delete(f"/labels/{label['id']}") # ---------- Helpers ---------- def assert_uuid(value: str): try: uuid.UUID(value) except (ValueError, TypeError): pytest.fail(f"'{value}' is not a valid UUID") def assert_timestamp(value: str): import datetime try: datetime.datetime.fromisoformat(value.replace('Z', '+00:00')) except ValueError: pytest.fail(f"'{value}' is not a valid ISO timestamp")