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