- Полный набор тестов для всех модулей API - test_auth.py: аутентификация и JWT токены - test_members.py: CRUD участников, агенты, токены - test_projects.py: CRUD проектов, участники проектов - test_tasks.py: CRUD задач, этапы, назначения, зависимости - test_chat.py: сообщения, комментарии, mentions - test_files.py: upload/download файлов проектов - test_labels.py: CRUD лейблов, привязка к задачам - test_websocket.py: WebSocket подключения и события - test_streaming.py: агентный стриминг через WebSocket - conftest.py: фикстуры для подключения к API - requirements.txt: зависимости pytest, httpx, websockets - pytest.ini: настройки asyncio для pytest
287 lines
10 KiB
Python
287 lines
10 KiB
Python
"""
|
||
Тесты работы с участниками - CRUD операции, смена slug, токены агентов
|
||
"""
|
||
import pytest
|
||
import httpx
|
||
import uuid
|
||
from conftest import assert_uuid, assert_timestamp
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_members_list(http_client: httpx.AsyncClient):
|
||
"""Test getting list of all members"""
|
||
response = await http_client.get("/members")
|
||
assert response.status_code == 200
|
||
|
||
members = response.json()
|
||
assert isinstance(members, list)
|
||
assert len(members) > 0
|
||
|
||
# Проверяем структуру первого участника
|
||
member = members[0]
|
||
assert "id" in member
|
||
assert "name" in member
|
||
assert "slug" in member
|
||
assert "type" in member
|
||
assert "role" in member
|
||
assert "status" in member
|
||
assert "is_active" in member
|
||
assert "last_seen_at" in member
|
||
|
||
assert_uuid(member["id"])
|
||
assert member["type"] in ["human", "agent", "bridge"]
|
||
assert member["role"] in ["owner", "admin", "member"]
|
||
assert member["status"] in ["online", "offline", "busy"]
|
||
assert isinstance(member["is_active"], bool)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_member_by_id(http_client: httpx.AsyncClient, test_user: dict):
|
||
"""Test getting specific member by ID"""
|
||
response = await http_client.get(f"/members/{test_user['id']}")
|
||
assert response.status_code == 200
|
||
|
||
member = response.json()
|
||
assert member["id"] == test_user["id"]
|
||
assert member["name"] == test_user["name"]
|
||
assert member["slug"] == test_user["slug"]
|
||
assert member["type"] == "human"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_member_not_found(http_client: httpx.AsyncClient):
|
||
"""Test getting non-existent member returns 404"""
|
||
fake_id = str(uuid.uuid4())
|
||
response = await http_client.get(f"/members/{fake_id}")
|
||
assert response.status_code == 404
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_human_member(http_client: httpx.AsyncClient):
|
||
"""Test creating new human member"""
|
||
member_slug = f"test-human-{uuid.uuid4().hex[:8]}"
|
||
member_data = {
|
||
"name": f"Test Human {member_slug}",
|
||
"slug": member_slug,
|
||
"type": "human",
|
||
"role": "member"
|
||
}
|
||
|
||
response = await http_client.post("/members", json=member_data)
|
||
assert response.status_code == 201
|
||
|
||
member = response.json()
|
||
assert member["name"] == member_data["name"]
|
||
assert member["slug"] == member_data["slug"]
|
||
assert member["type"] == "human"
|
||
assert member["role"] == "member"
|
||
assert_uuid(member["id"])
|
||
|
||
# У человека не должно быть токена
|
||
assert "token" not in member
|
||
|
||
# Cleanup
|
||
await http_client.delete(f"/members/{member['id']}")
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_agent_member(http_client: httpx.AsyncClient):
|
||
"""Test creating new agent member with configuration"""
|
||
agent_slug = f"test-agent-{uuid.uuid4().hex[:8]}"
|
||
agent_data = {
|
||
"name": f"Test Agent {agent_slug}",
|
||
"slug": agent_slug,
|
||
"type": "agent",
|
||
"role": "member",
|
||
"agent_config": {
|
||
"capabilities": ["coding", "testing", "documentation"],
|
||
"labels": ["backend", "python", "api"],
|
||
"chat_listen": "mentions",
|
||
"task_listen": "assigned",
|
||
"prompt": "You are a test automation agent",
|
||
"model": "gpt-4"
|
||
}
|
||
}
|
||
|
||
response = await http_client.post("/members", json=agent_data)
|
||
assert response.status_code == 201
|
||
|
||
agent = response.json()
|
||
assert agent["name"] == agent_data["name"]
|
||
assert agent["slug"] == agent_data["slug"]
|
||
assert agent["type"] == "agent"
|
||
assert agent["role"] == "member"
|
||
assert_uuid(agent["id"])
|
||
|
||
# У агента должен быть токен при создании
|
||
assert "token" in agent
|
||
assert agent["token"].startswith("tb-")
|
||
|
||
# Проверяем конфигурацию агента
|
||
config = agent["agent_config"]
|
||
assert config["capabilities"] == agent_data["agent_config"]["capabilities"]
|
||
assert config["labels"] == agent_data["agent_config"]["labels"]
|
||
assert config["chat_listen"] == "mentions"
|
||
assert config["task_listen"] == "assigned"
|
||
|
||
# Cleanup
|
||
await http_client.delete(f"/members/{agent['id']}")
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_member_duplicate_slug(http_client: httpx.AsyncClient, test_user: dict):
|
||
"""Test creating member with duplicate slug returns 409"""
|
||
member_data = {
|
||
"name": "Another User",
|
||
"slug": test_user["slug"], # Используем существующий slug
|
||
"type": "human",
|
||
"role": "member"
|
||
}
|
||
|
||
response = await http_client.post("/members", json=member_data)
|
||
assert response.status_code == 409
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_update_member_info(http_client: httpx.AsyncClient, test_user: dict):
|
||
"""Test updating member information"""
|
||
update_data = {
|
||
"name": "Updated Test User",
|
||
"role": "admin"
|
||
}
|
||
|
||
response = await http_client.patch(f"/members/{test_user['id']}", json=update_data)
|
||
assert response.status_code == 200
|
||
|
||
member = response.json()
|
||
assert member["name"] == "Updated Test User"
|
||
assert member["role"] == "admin"
|
||
assert member["id"] == test_user["id"]
|
||
assert member["slug"] == test_user["slug"] # slug не изменился
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_update_member_slug(http_client: httpx.AsyncClient, test_user: dict):
|
||
"""Test updating member slug"""
|
||
new_slug = f"updated-slug-{uuid.uuid4().hex[:8]}"
|
||
update_data = {"slug": new_slug}
|
||
|
||
response = await http_client.patch(f"/members/{test_user['id']}", json=update_data)
|
||
assert response.status_code == 200
|
||
|
||
member = response.json()
|
||
assert member["slug"] == new_slug
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_update_agent_config(http_client: httpx.AsyncClient, test_agent: dict):
|
||
"""Test updating agent configuration"""
|
||
new_config = {
|
||
"agent_config": {
|
||
"capabilities": ["testing", "debugging"],
|
||
"labels": ["frontend", "javascript"],
|
||
"chat_listen": "all",
|
||
"task_listen": "mentions",
|
||
"prompt": "Updated test agent",
|
||
"model": "claude-3"
|
||
}
|
||
}
|
||
|
||
response = await http_client.patch(f"/members/{test_agent['id']}", json=new_config)
|
||
assert response.status_code == 200
|
||
|
||
agent = response.json()
|
||
config = agent["agent_config"]
|
||
assert config["capabilities"] == new_config["agent_config"]["capabilities"]
|
||
assert config["labels"] == new_config["agent_config"]["labels"]
|
||
assert config["chat_listen"] == "all"
|
||
assert config["task_listen"] == "mentions"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_regenerate_agent_token(http_client: httpx.AsyncClient, test_agent: dict):
|
||
"""Test regenerating agent token"""
|
||
response = await http_client.post(f"/members/{test_agent['id']}/regenerate-token")
|
||
assert response.status_code == 200
|
||
|
||
data = response.json()
|
||
assert "token" in data
|
||
assert data["token"].startswith("tb-")
|
||
assert data["token"] != test_agent["token"] # Новый токен должен отличаться
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_regenerate_token_for_human_fails(http_client: httpx.AsyncClient, test_user: dict):
|
||
"""Test that regenerating token for human returns 400"""
|
||
response = await http_client.post(f"/members/{test_user['id']}/regenerate-token")
|
||
assert response.status_code == 400
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_revoke_agent_token(http_client: httpx.AsyncClient, test_agent: dict):
|
||
"""Test revoking agent token"""
|
||
response = await http_client.post(f"/members/{test_agent['id']}/revoke-token")
|
||
assert response.status_code == 200
|
||
|
||
data = response.json()
|
||
assert data["ok"] is True
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_revoke_token_for_human_fails(http_client: httpx.AsyncClient, test_user: dict):
|
||
"""Test that revoking token for human returns 400"""
|
||
response = await http_client.post(f"/members/{test_user['id']}/revoke-token")
|
||
assert response.status_code == 400
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_update_member_status(agent_client: httpx.AsyncClient):
|
||
"""Test updating member status (for agents)"""
|
||
response = await agent_client.patch("/members/me/status?status=busy")
|
||
assert response.status_code == 200
|
||
|
||
data = response.json()
|
||
assert data["status"] == "busy"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_delete_member(http_client: httpx.AsyncClient):
|
||
"""Test soft deleting member (sets is_active=false)"""
|
||
# Создаём временного пользователя для удаления
|
||
member_slug = f"to-delete-{uuid.uuid4().hex[:8]}"
|
||
member_data = {
|
||
"name": f"User to Delete {member_slug}",
|
||
"slug": member_slug,
|
||
"type": "human",
|
||
"role": "member"
|
||
}
|
||
|
||
response = await http_client.post("/members", json=member_data)
|
||
assert response.status_code == 201
|
||
member = response.json()
|
||
|
||
# Удаляем пользователя
|
||
response = await http_client.delete(f"/members/{member['id']}")
|
||
assert response.status_code == 200
|
||
|
||
data = response.json()
|
||
assert data["ok"] is True
|
||
|
||
# Проверяем что пользователь помечен неактивным
|
||
response = await http_client.get(f"/members/{member['id']}")
|
||
assert response.status_code == 200
|
||
deleted_member = response.json()
|
||
assert deleted_member["is_active"] is False
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_members_include_inactive(http_client: httpx.AsyncClient):
|
||
"""Test getting members list with inactive ones"""
|
||
response = await http_client.get("/members?include_inactive=true")
|
||
assert response.status_code == 200
|
||
|
||
members = response.json()
|
||
# Должен содержать как активных, так и неактивных
|
||
has_inactive = any(not m["is_active"] for m in members)
|
||
# Может и не быть неактивных, но запрос должен пройти
|
||
assert isinstance(members, list) |