docs/tests/test_tasks.py
markov 2bab3cf60a Добавлены E2E тесты для Team Board API
- Полный набор тестов для всех модулей 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
2026-03-13 22:47:19 +01:00

487 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Тесты работы с задачами - 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