- Полный набор тестов для всех модулей 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
487 lines
17 KiB
Python
487 lines
17 KiB
Python
"""
|
||
Тесты работы с задачами - CRUD операции, статусы, назначения, этапы, зависимости
|
||
"""
|
||
import pytest
|
||
import httpx
|
||
import uuid
|
||
from conftest import assert_uuid, assert_timestamp
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_tasks_list(http_client: httpx.AsyncClient):
|
||
"""Test getting list of all tasks"""
|
||
response = await http_client.get("/tasks")
|
||
assert response.status_code == 200
|
||
|
||
tasks = response.json()
|
||
assert isinstance(tasks, list)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_tasks_by_project(http_client: httpx.AsyncClient, test_project: dict, test_task: dict):
|
||
"""Test filtering tasks by project"""
|
||
response = await http_client.get(f"/tasks?project_id={test_project['id']}")
|
||
assert response.status_code == 200
|
||
|
||
tasks = response.json()
|
||
assert isinstance(tasks, list)
|
||
|
||
# Все задачи должны быть из нашего проекта
|
||
for task in tasks:
|
||
assert task["project"]["id"] == test_project["id"]
|
||
|
||
# Наша тестовая задача должна быть в списке
|
||
task_ids = [t["id"] for t in tasks]
|
||
assert test_task["id"] in task_ids
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_task_by_id(http_client: httpx.AsyncClient, test_task: dict):
|
||
"""Test getting specific task by ID"""
|
||
response = await http_client.get(f"/tasks/{test_task['id']}")
|
||
assert response.status_code == 200
|
||
|
||
task = response.json()
|
||
assert task["id"] == test_task["id"]
|
||
assert task["title"] == test_task["title"]
|
||
assert task["description"] == test_task["description"]
|
||
assert task["status"] == test_task["status"]
|
||
|
||
# Проверяем структуру
|
||
assert "project" in task
|
||
assert "number" in task
|
||
assert "key" in task
|
||
assert "type" in task
|
||
assert "priority" in task
|
||
assert "labels" in task
|
||
assert "assignee" in task
|
||
assert "reviewer" in task
|
||
assert "subtasks" in task
|
||
assert "steps" in task
|
||
assert "watcher_ids" in task
|
||
assert "depends_on" in task
|
||
assert "position" in task
|
||
assert "created_at" in task
|
||
assert "updated_at" in task
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_task_not_found(http_client: httpx.AsyncClient):
|
||
"""Test getting non-existent task returns 404"""
|
||
fake_id = str(uuid.uuid4())
|
||
response = await http_client.get(f"/tasks/{fake_id}")
|
||
assert response.status_code == 404
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_task(http_client: httpx.AsyncClient, test_project: dict):
|
||
"""Test creating new task"""
|
||
task_data = {
|
||
"title": "New Test Task",
|
||
"description": "Task created via API test",
|
||
"type": "task",
|
||
"status": "backlog",
|
||
"priority": "high",
|
||
"labels": ["backend", "urgent"]
|
||
}
|
||
|
||
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||
assert response.status_code == 201
|
||
|
||
task = response.json()
|
||
assert task["title"] == task_data["title"]
|
||
assert task["description"] == task_data["description"]
|
||
assert task["type"] == task_data["type"]
|
||
assert task["status"] == task_data["status"]
|
||
assert task["priority"] == task_data["priority"]
|
||
assert task["labels"] == task_data["labels"]
|
||
|
||
# Проверяем автогенерируемые поля
|
||
assert_uuid(task["id"])
|
||
assert isinstance(task["number"], int)
|
||
assert task["number"] > 0
|
||
assert task["key"].startswith(test_project["slug"].upper())
|
||
assert f"-{task['number']}" in task["key"]
|
||
|
||
# Проверяем связанный проект
|
||
assert task["project"]["id"] == test_project["id"]
|
||
assert task["project"]["slug"] == test_project["slug"]
|
||
|
||
# По умолчанию assignee и reviewer должны быть null
|
||
assert task["assignee"] is None
|
||
assert task["reviewer"] is None
|
||
|
||
# Массивы должны быть пустыми
|
||
assert task["subtasks"] == []
|
||
assert task["steps"] == []
|
||
assert task["watcher_ids"] == []
|
||
assert task["depends_on"] == []
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_task_minimal(http_client: httpx.AsyncClient, test_project: dict):
|
||
"""Test creating task with minimal required data"""
|
||
task_data = {"title": "Minimal Task"}
|
||
|
||
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||
assert response.status_code == 201
|
||
|
||
task = response.json()
|
||
assert task["title"] == "Minimal Task"
|
||
assert task["type"] == "task" # По умолчанию
|
||
assert task["status"] == "backlog" # По умолчанию
|
||
assert task["priority"] == "medium" # По умолчанию
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_task_with_assignee(http_client: httpx.AsyncClient, test_project: dict, test_user: dict):
|
||
"""Test creating task with assignee"""
|
||
task_data = {
|
||
"title": "Assigned Task",
|
||
"assignee_id": test_user["id"]
|
||
}
|
||
|
||
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||
assert response.status_code == 201
|
||
|
||
task = response.json()
|
||
assert task["assignee"]["id"] == test_user["id"]
|
||
assert task["assignee"]["slug"] == test_user["slug"]
|
||
assert task["assignee"]["name"] == test_user["name"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_task_without_project_fails(http_client: httpx.AsyncClient):
|
||
"""Test creating task without project_id fails"""
|
||
task_data = {"title": "Task without project"}
|
||
|
||
response = await http_client.post("/tasks", json=task_data)
|
||
assert response.status_code == 422 # Missing query parameter
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_update_task(http_client: httpx.AsyncClient, test_task: dict):
|
||
"""Test updating task information"""
|
||
update_data = {
|
||
"title": "Updated Task Title",
|
||
"description": "Updated description",
|
||
"status": "in_progress",
|
||
"priority": "high",
|
||
"labels": ["updated", "test"]
|
||
}
|
||
|
||
response = await http_client.patch(f"/tasks/{test_task['id']}", json=update_data)
|
||
assert response.status_code == 200
|
||
|
||
task = response.json()
|
||
assert task["title"] == "Updated Task Title"
|
||
assert task["description"] == "Updated description"
|
||
assert task["status"] == "in_progress"
|
||
assert task["priority"] == "high"
|
||
assert task["labels"] == ["updated", "test"]
|
||
assert task["id"] == test_task["id"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_assign_task(http_client: httpx.AsyncClient, test_task: dict, test_user: dict):
|
||
"""Test assigning task to user"""
|
||
response = await http_client.post(f"/tasks/{test_task['id']}/assign", json={
|
||
"assignee_id": test_user["id"]
|
||
})
|
||
assert response.status_code == 200
|
||
|
||
task = response.json()
|
||
assert task["assignee"]["id"] == test_user["id"]
|
||
assert task["assignee"]["slug"] == test_user["slug"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_assign_task_nonexistent_user(http_client: httpx.AsyncClient, test_task: dict):
|
||
"""Test assigning task to non-existent user returns 404"""
|
||
fake_user_id = str(uuid.uuid4())
|
||
response = await http_client.post(f"/tasks/{test_task['id']}/assign", json={
|
||
"assignee_id": fake_user_id
|
||
})
|
||
assert response.status_code == 404
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_take_task_by_agent(agent_client: httpx.AsyncClient, test_project: dict):
|
||
"""Test agent taking unassigned task"""
|
||
# Создаём неназначенную задачу
|
||
task_data = {
|
||
"title": "Task for Agent",
|
||
"status": "todo" # Подходящий статус для взятия
|
||
}
|
||
|
||
response = await agent_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||
assert response.status_code == 201
|
||
task = response.json()
|
||
|
||
# Берём задачу в работу
|
||
response = await agent_client.post(f"/tasks/{task['id']}/take")
|
||
assert response.status_code == 200
|
||
|
||
updated_task = response.json()
|
||
assert updated_task["status"] == "in_progress"
|
||
# assignee_id должен быть установлен на текущего агента
|
||
assert updated_task["assignee"] is not None
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_reject_assigned_task(agent_client: httpx.AsyncClient, test_project: dict):
|
||
"""Test agent rejecting assigned task"""
|
||
# Создаём задачу и назначаем её на агента
|
||
task_data = {
|
||
"title": "Task to Reject",
|
||
"status": "in_progress"
|
||
}
|
||
|
||
response = await agent_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||
assert response.status_code == 201
|
||
task = response.json()
|
||
|
||
# Отклоняем задачу
|
||
response = await agent_client.post(f"/tasks/{task['id']}/reject", json={
|
||
"reason": "Too complex for my current capabilities"
|
||
})
|
||
assert response.status_code == 200
|
||
|
||
data = response.json()
|
||
assert data["ok"] is True
|
||
assert data["reason"] == "Too complex for my current capabilities"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_add_task_watcher(http_client: httpx.AsyncClient, test_task: dict):
|
||
"""Test adding watcher to task"""
|
||
response = await http_client.post(f"/tasks/{test_task['id']}/watch")
|
||
assert response.status_code == 200
|
||
|
||
data = response.json()
|
||
assert data["ok"] is True
|
||
assert isinstance(data["watcher_ids"], list)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_remove_task_watcher(http_client: httpx.AsyncClient, test_task: dict):
|
||
"""Test removing watcher from task"""
|
||
# Сначала добавляем себя как наблюдателя
|
||
await http_client.post(f"/tasks/{test_task['id']}/watch")
|
||
|
||
# Затем удаляем
|
||
response = await http_client.delete(f"/tasks/{test_task['id']}/watch")
|
||
assert response.status_code == 200
|
||
|
||
data = response.json()
|
||
assert data["ok"] is True
|
||
assert isinstance(data["watcher_ids"], list)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_filter_tasks_by_status(http_client: httpx.AsyncClient, test_project: dict):
|
||
"""Test filtering tasks by status"""
|
||
# Создаём задачи с разными статусами
|
||
statuses = ["backlog", "todo", "in_progress", "done"]
|
||
created_tasks = []
|
||
|
||
for status in statuses:
|
||
task_data = {
|
||
"title": f"Task {status}",
|
||
"status": status
|
||
}
|
||
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||
assert response.status_code == 201
|
||
created_tasks.append(response.json())
|
||
|
||
# Фильтруем по статусу "in_progress"
|
||
response = await http_client.get(f"/tasks?project_id={test_project['id']}&status=in_progress")
|
||
assert response.status_code == 200
|
||
|
||
tasks = response.json()
|
||
for task in tasks:
|
||
assert task["status"] == "in_progress"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_filter_tasks_by_assignee(http_client: httpx.AsyncClient, test_project: dict, test_user: dict):
|
||
"""Test filtering tasks by assignee"""
|
||
# Создаём задачу и назначаем её на пользователя
|
||
task_data = {
|
||
"title": "Task for specific user",
|
||
"assignee_id": test_user["id"]
|
||
}
|
||
|
||
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||
assert response.status_code == 201
|
||
task = response.json()
|
||
|
||
# Фильтруем по исполнителю
|
||
response = await http_client.get(f"/tasks?assignee_id={test_user['id']}")
|
||
assert response.status_code == 200
|
||
|
||
tasks = response.json()
|
||
for task in tasks:
|
||
assert task["assignee"]["id"] == test_user["id"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_search_tasks_by_number(http_client: httpx.AsyncClient, test_task: dict):
|
||
"""Test searching tasks by task number"""
|
||
task_key = test_task["key"]
|
||
|
||
response = await http_client.get(f"/tasks?q={task_key}")
|
||
assert response.status_code == 200
|
||
|
||
tasks = response.json()
|
||
task_keys = [t["key"] for t in tasks]
|
||
assert task_key in task_keys
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_search_tasks_by_title(http_client: httpx.AsyncClient, test_task: dict):
|
||
"""Test searching tasks by title"""
|
||
search_term = test_task["title"].split()[0] # Первое слово из заголовка
|
||
|
||
response = await http_client.get(f"/tasks?q={search_term}")
|
||
assert response.status_code == 200
|
||
|
||
tasks = response.json()
|
||
# Хотя бы одна задача должна содержать искомое слово
|
||
found = any(search_term.lower() in task["title"].lower() for task in tasks)
|
||
assert found
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_delete_task(http_client: httpx.AsyncClient, test_project: dict):
|
||
"""Test deleting task"""
|
||
# Создаём временную задачу для удаления
|
||
task_data = {"title": "Task to Delete"}
|
||
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||
assert response.status_code == 201
|
||
task = response.json()
|
||
|
||
# Удаляем задачу
|
||
response = await http_client.delete(f"/tasks/{task['id']}")
|
||
assert response.status_code == 200
|
||
|
||
data = response.json()
|
||
assert data["ok"] is True
|
||
|
||
# Проверяем что задача действительно удалена
|
||
response = await http_client.get(f"/tasks/{task['id']}")
|
||
assert response.status_code == 404
|
||
|
||
|
||
# Тесты этапов (Steps)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_task_steps(http_client: httpx.AsyncClient, test_task: dict):
|
||
"""Test getting task steps list"""
|
||
response = await http_client.get(f"/tasks/{test_task['id']}/steps")
|
||
assert response.status_code == 200
|
||
|
||
steps = response.json()
|
||
assert isinstance(steps, list)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_task_step(http_client: httpx.AsyncClient, test_task: dict):
|
||
"""Test creating new task step"""
|
||
step_data = {"title": "Complete step 1"}
|
||
|
||
response = await http_client.post(f"/tasks/{test_task['id']}/steps", json=step_data)
|
||
assert response.status_code == 201
|
||
|
||
step = response.json()
|
||
assert step["title"] == "Complete step 1"
|
||
assert step["done"] is False # По умолчанию
|
||
assert isinstance(step["position"], int)
|
||
assert_uuid(step["id"])
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_update_task_step(http_client: httpx.AsyncClient, test_task: dict):
|
||
"""Test updating task step"""
|
||
# Создаём этап
|
||
step_data = {"title": "Step to update"}
|
||
response = await http_client.post(f"/tasks/{test_task['id']}/steps", json=step_data)
|
||
assert response.status_code == 201
|
||
step = response.json()
|
||
|
||
# Обновляем этап
|
||
update_data = {
|
||
"title": "Updated step title",
|
||
"done": True
|
||
}
|
||
|
||
response = await http_client.patch(f"/tasks/{test_task['id']}/steps/{step['id']}", json=update_data)
|
||
assert response.status_code == 200
|
||
|
||
updated_step = response.json()
|
||
assert updated_step["title"] == "Updated step title"
|
||
assert updated_step["done"] is True
|
||
assert updated_step["id"] == step["id"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_delete_task_step(http_client: httpx.AsyncClient, test_task: dict):
|
||
"""Test deleting task step"""
|
||
# Создаём этап
|
||
step_data = {"title": "Step to delete"}
|
||
response = await http_client.post(f"/tasks/{test_task['id']}/steps", json=step_data)
|
||
assert response.status_code == 201
|
||
step = response.json()
|
||
|
||
# Удаляем этап
|
||
response = await http_client.delete(f"/tasks/{test_task['id']}/steps/{step['id']}")
|
||
assert response.status_code == 200
|
||
|
||
data = response.json()
|
||
assert data["ok"] is True
|
||
|
||
|
||
# Тесты связей между задачами
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_task_link(http_client: httpx.AsyncClient, test_project: dict):
|
||
"""Test creating dependency between tasks"""
|
||
# Создаём две задачи
|
||
task1_data = {"title": "Source Task"}
|
||
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task1_data)
|
||
assert response.status_code == 201
|
||
task1 = response.json()
|
||
|
||
task2_data = {"title": "Target Task"}
|
||
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task2_data)
|
||
assert response.status_code == 201
|
||
task2 = response.json()
|
||
|
||
# Создаём связь "task1 зависит от task2"
|
||
link_data = {
|
||
"target_id": task2["id"],
|
||
"link_type": "depends_on"
|
||
}
|
||
|
||
response = await http_client.post(f"/tasks/{task1['id']}/links", json=link_data)
|
||
assert response.status_code == 201
|
||
|
||
link = response.json()
|
||
assert link["source_id"] == task1["id"]
|
||
assert link["target_id"] == task2["id"]
|
||
assert link["link_type"] == "depends_on"
|
||
assert link["target_key"] == task2["key"]
|
||
assert link["source_key"] == task1["key"]
|
||
assert_uuid(link["id"])
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_task_link_self_reference_fails(http_client: httpx.AsyncClient, test_task: dict):
|
||
"""Test that linking task to itself returns 400"""
|
||
link_data = {
|
||
"target_id": test_task["id"], # Ссылка на себя
|
||
"link_type": "depends_on"
|
||
}
|
||
|
||
response = await http_client.post(f"/tasks/{test_task['id']}/links", json=link_data)
|
||
assert response.status_code == 400 |