- Полный набор тестов для всех модулей 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
352 lines
13 KiB
Python
352 lines
13 KiB
Python
"""
|
||
Тесты работы с лейблами - CRUD операции, привязка к задачам
|
||
"""
|
||
import pytest
|
||
import httpx
|
||
import uuid
|
||
from conftest import assert_uuid
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_labels_list(http_client: httpx.AsyncClient):
|
||
"""Test getting list of all labels"""
|
||
response = await http_client.get("/labels")
|
||
assert response.status_code == 200
|
||
|
||
labels = response.json()
|
||
assert isinstance(labels, list)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_label(http_client: httpx.AsyncClient):
|
||
"""Test creating new label"""
|
||
label_name = f"test-label-{uuid.uuid4().hex[:8]}"
|
||
label_data = {
|
||
"name": label_name,
|
||
"color": "#ff5733"
|
||
}
|
||
|
||
response = await http_client.post("/labels", json=label_data)
|
||
assert response.status_code == 201
|
||
|
||
label = response.json()
|
||
assert label["name"] == label_name
|
||
assert label["color"] == "#ff5733"
|
||
assert_uuid(label["id"])
|
||
|
||
# Cleanup
|
||
await http_client.delete(f"/labels/{label['id']}")
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_label_default_color(http_client: httpx.AsyncClient):
|
||
"""Test creating label without color uses default"""
|
||
label_name = f"default-color-{uuid.uuid4().hex[:8]}"
|
||
label_data = {"name": label_name}
|
||
|
||
response = await http_client.post("/labels", json=label_data)
|
||
assert response.status_code == 201
|
||
|
||
label = response.json()
|
||
assert label["name"] == label_name
|
||
assert label["color"] == "#6366f1" # Цвет по умолчанию
|
||
|
||
# Cleanup
|
||
await http_client.delete(f"/labels/{label['id']}")
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_label_duplicate_name_fails(http_client: httpx.AsyncClient, test_label: dict):
|
||
"""Test creating label with duplicate name returns 409"""
|
||
label_data = {
|
||
"name": test_label["name"], # Используем существующее имя
|
||
"color": "#123456"
|
||
}
|
||
|
||
response = await http_client.post("/labels", json=label_data)
|
||
assert response.status_code == 409
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_update_label(http_client: httpx.AsyncClient, test_label: dict):
|
||
"""Test updating label name and color"""
|
||
update_data = {
|
||
"name": f"updated-{test_label['name']}",
|
||
"color": "#00ff00"
|
||
}
|
||
|
||
response = await http_client.patch(f"/labels/{test_label['id']}", json=update_data)
|
||
assert response.status_code == 200
|
||
|
||
label = response.json()
|
||
assert label["name"] == update_data["name"]
|
||
assert label["color"] == update_data["color"]
|
||
assert label["id"] == test_label["id"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_update_label_name_only(http_client: httpx.AsyncClient, test_label: dict):
|
||
"""Test updating only label name"""
|
||
original_color = test_label["color"]
|
||
update_data = {"name": f"name-only-{uuid.uuid4().hex[:8]}"}
|
||
|
||
response = await http_client.patch(f"/labels/{test_label['id']}", json=update_data)
|
||
assert response.status_code == 200
|
||
|
||
label = response.json()
|
||
assert label["name"] == update_data["name"]
|
||
assert label["color"] == original_color # Цвет не изменился
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_update_label_color_only(http_client: httpx.AsyncClient, test_label: dict):
|
||
"""Test updating only label color"""
|
||
original_name = test_label["name"]
|
||
update_data = {"color": "#purple"}
|
||
|
||
response = await http_client.patch(f"/labels/{test_label['id']}", json=update_data)
|
||
assert response.status_code == 200
|
||
|
||
label = response.json()
|
||
assert label["name"] == original_name # Имя не изменилось
|
||
assert label["color"] == "#purple"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_update_nonexistent_label(http_client: httpx.AsyncClient):
|
||
"""Test updating non-existent label returns 404"""
|
||
fake_label_id = str(uuid.uuid4())
|
||
update_data = {"name": "nonexistent"}
|
||
|
||
response = await http_client.patch(f"/labels/{fake_label_id}", json=update_data)
|
||
assert response.status_code == 404
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_delete_label(http_client: httpx.AsyncClient):
|
||
"""Test deleting label"""
|
||
# Создаём временный лейбл для удаления
|
||
label_name = f"to-delete-{uuid.uuid4().hex[:8]}"
|
||
label_data = {"name": label_name, "color": "#ff0000"}
|
||
|
||
response = await http_client.post("/labels", json=label_data)
|
||
assert response.status_code == 201
|
||
label = response.json()
|
||
|
||
# Удаляем лейбл
|
||
response = await http_client.delete(f"/labels/{label['id']}")
|
||
assert response.status_code == 200
|
||
|
||
data = response.json()
|
||
assert data["ok"] is True
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_delete_nonexistent_label(http_client: httpx.AsyncClient):
|
||
"""Test deleting non-existent label returns 404"""
|
||
fake_label_id = str(uuid.uuid4())
|
||
|
||
response = await http_client.delete(f"/labels/{fake_label_id}")
|
||
assert response.status_code == 404
|
||
|
||
|
||
# Тесты привязки лейблов к задачам
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_add_label_to_task(http_client: httpx.AsyncClient, test_task: dict, test_label: dict):
|
||
"""Test adding label to task"""
|
||
response = await http_client.post(f"/tasks/{test_task['id']}/labels/{test_label['id']}")
|
||
assert response.status_code == 200
|
||
|
||
data = response.json()
|
||
assert data["ok"] is True
|
||
|
||
# Проверяем что лейбл добавился к задаче
|
||
task_response = await http_client.get(f"/tasks/{test_task['id']}")
|
||
assert task_response.status_code == 200
|
||
|
||
task = task_response.json()
|
||
assert test_label["name"] in task["labels"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_add_multiple_labels_to_task(http_client: httpx.AsyncClient, test_task: dict):
|
||
"""Test adding multiple labels to task"""
|
||
# Создаём несколько лейблов
|
||
labels_created = []
|
||
for i in range(3):
|
||
label_data = {
|
||
"name": f"multi-label-{i}-{uuid.uuid4().hex[:6]}",
|
||
"color": f"#{i:02d}{i:02d}{i:02d}"
|
||
}
|
||
response = await http_client.post("/labels", json=label_data)
|
||
assert response.status_code == 201
|
||
labels_created.append(response.json())
|
||
|
||
# Добавляем все лейблы к задаче
|
||
for label in labels_created:
|
||
response = await http_client.post(f"/tasks/{test_task['id']}/labels/{label['id']}")
|
||
assert response.status_code == 200
|
||
|
||
# Проверяем что все лейблы добавились
|
||
task_response = await http_client.get(f"/tasks/{test_task['id']}")
|
||
assert task_response.status_code == 200
|
||
|
||
task = task_response.json()
|
||
for label in labels_created:
|
||
assert label["name"] in task["labels"]
|
||
|
||
# Cleanup
|
||
for label in labels_created:
|
||
await http_client.delete(f"/labels/{label['id']}")
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_add_nonexistent_label_to_task(http_client: httpx.AsyncClient, test_task: dict):
|
||
"""Test adding non-existent label to task returns 404"""
|
||
fake_label_id = str(uuid.uuid4())
|
||
|
||
response = await http_client.post(f"/tasks/{test_task['id']}/labels/{fake_label_id}")
|
||
assert response.status_code == 404
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_add_label_to_nonexistent_task(http_client: httpx.AsyncClient, test_label: dict):
|
||
"""Test adding label to non-existent task returns 404"""
|
||
fake_task_id = str(uuid.uuid4())
|
||
|
||
response = await http_client.post(f"/tasks/{fake_task_id}/labels/{test_label['id']}")
|
||
assert response.status_code == 404
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_add_duplicate_label_to_task(http_client: httpx.AsyncClient, test_task: dict, test_label: dict):
|
||
"""Test adding same label twice to task (should be idempotent)"""
|
||
# Добавляем лейбл первый раз
|
||
response = await http_client.post(f"/tasks/{test_task['id']}/labels/{test_label['id']}")
|
||
assert response.status_code == 200
|
||
|
||
# Добавляем тот же лейбл повторно
|
||
response = await http_client.post(f"/tasks/{test_task['id']}/labels/{test_label['id']}")
|
||
# В зависимости от реализации может быть 200 (идемпотентность) или 409 (конфликт)
|
||
assert response.status_code in [200, 409]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_remove_label_from_task(http_client: httpx.AsyncClient, test_task: dict, test_label: dict):
|
||
"""Test removing label from task"""
|
||
# Сначала добавляем лейбл
|
||
response = await http_client.post(f"/tasks/{test_task['id']}/labels/{test_label['id']}")
|
||
assert response.status_code == 200
|
||
|
||
# Затем удаляем
|
||
response = await http_client.delete(f"/tasks/{test_task['id']}/labels/{test_label['id']}")
|
||
assert response.status_code == 200
|
||
|
||
data = response.json()
|
||
assert data["ok"] is True
|
||
|
||
# Проверяем что лейбл удалился
|
||
task_response = await http_client.get(f"/tasks/{test_task['id']}")
|
||
assert task_response.status_code == 200
|
||
|
||
task = task_response.json()
|
||
assert test_label["name"] not in task["labels"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_remove_nonexistent_label_from_task(http_client: httpx.AsyncClient, test_task: dict):
|
||
"""Test removing non-existent label from task returns 404"""
|
||
fake_label_id = str(uuid.uuid4())
|
||
|
||
response = await http_client.delete(f"/tasks/{test_task['id']}/labels/{fake_label_id}")
|
||
assert response.status_code == 404
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_remove_label_from_nonexistent_task(http_client: httpx.AsyncClient, test_label: dict):
|
||
"""Test removing label from non-existent task returns 404"""
|
||
fake_task_id = str(uuid.uuid4())
|
||
|
||
response = await http_client.delete(f"/tasks/{fake_task_id}/labels/{test_label['id']}")
|
||
assert response.status_code == 404
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_remove_label_not_attached_to_task(http_client: httpx.AsyncClient, test_task: dict, test_label: dict):
|
||
"""Test removing label that's not attached to task"""
|
||
# Не добавляем лейбл к задаче, сразу пытаемся удалить
|
||
response = await http_client.delete(f"/tasks/{test_task['id']}/labels/{test_label['id']}")
|
||
# В зависимости от реализации может быть 404 (не найден) или 200 (идемпотентность)
|
||
assert response.status_code in [200, 404]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_filter_tasks_by_label(http_client: httpx.AsyncClient, test_project: dict):
|
||
"""Test filtering tasks by label"""
|
||
# Создаём лейбл для фильтрации
|
||
label_data = {
|
||
"name": f"filter-test-{uuid.uuid4().hex[:8]}",
|
||
"color": "#123456"
|
||
}
|
||
response = await http_client.post("/labels", json=label_data)
|
||
assert response.status_code == 201
|
||
filter_label = response.json()
|
||
|
||
# Создаём задачу с лейблом
|
||
task_data = {
|
||
"title": "Task with specific label",
|
||
"labels": [filter_label["name"]]
|
||
}
|
||
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||
assert response.status_code == 201
|
||
labeled_task = response.json()
|
||
|
||
# Фильтруем задачи по лейблу
|
||
response = await http_client.get(f"/tasks?label={filter_label['name']}")
|
||
assert response.status_code == 200
|
||
|
||
tasks = response.json()
|
||
# Все найденные задачи должны содержать указанный лейбл
|
||
for task in tasks:
|
||
assert filter_label["name"] in task["labels"]
|
||
|
||
# Наша задача должна быть в результатах
|
||
task_ids = [t["id"] for t in tasks]
|
||
assert labeled_task["id"] in task_ids
|
||
|
||
# Cleanup
|
||
await http_client.delete(f"/labels/{filter_label['id']}")
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_task_with_labels(http_client: httpx.AsyncClient, test_project: dict):
|
||
"""Test creating task with labels from the start"""
|
||
# Создаём несколько лейблов
|
||
labels_data = [
|
||
{"name": f"initial-label-1-{uuid.uuid4().hex[:6]}", "color": "#ff0000"},
|
||
{"name": f"initial-label-2-{uuid.uuid4().hex[:6]}", "color": "#00ff00"}
|
||
]
|
||
|
||
created_labels = []
|
||
for label_data in labels_data:
|
||
response = await http_client.post("/labels", json=label_data)
|
||
assert response.status_code == 201
|
||
created_labels.append(response.json())
|
||
|
||
# Создаём задачу с лейблами
|
||
task_data = {
|
||
"title": "Task with initial labels",
|
||
"labels": [label["name"] for label in created_labels]
|
||
}
|
||
|
||
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||
assert response.status_code == 201
|
||
|
||
task = response.json()
|
||
for label in created_labels:
|
||
assert label["name"] in task["labels"]
|
||
|
||
# Cleanup
|
||
for label in created_labels:
|
||
await http_client.delete(f"/labels/{label['id']}") |