180 lines
5.9 KiB
Python
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")
|