docs/tests/conftest.py

180 lines
5.9 KiB
Python

"""
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")