"""Tasks plugin tests — CRUD, assign, status update, filtering.""" from __future__ import annotations import pytest from httpx import AsyncClient from tests.conftest import ORIGIN_HEADER, login_client, seed_tenant_and_users @pytest.mark.asyncio class TestTaskList: """GET /api/v1/tasks""" async def test_list_tasks_returns_200(self, client: AsyncClient, db_session): """GET /tasks returns 200 with paginated list.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") resp = await client.get("/api/v1/tasks", headers=ORIGIN_HEADER) assert resp.status_code == 200 data = resp.json() assert "items" in data assert "total" in data assert "page" in data assert "page_size" in data async def test_list_tasks_with_status_filter(self, client: AsyncClient, db_session): """GET /tasks?status=open filters by status.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") resp = await client.get("/api/v1/tasks?status=open", headers=ORIGIN_HEADER) assert resp.status_code == 200 for item in resp.json()["items"]: assert item["status"] == "open" async def test_list_tasks_requires_auth(self, client: AsyncClient, db_session): """GET /tasks without auth returns 401.""" resp = await client.get("/api/v1/tasks", headers=ORIGIN_HEADER) assert resp.status_code == 401 @pytest.mark.asyncio class TestTaskCreate: """POST /api/v1/tasks""" async def test_create_task_returns_201(self, client: AsyncClient, db_session): """POST /tasks creates a task and returns 201.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") resp = await client.post( "/api/v1/tasks", json={"title": "Call customer", "priority": "high"}, headers=ORIGIN_HEADER, ) assert resp.status_code == 201 data = resp.json() assert data["title"] == "Call customer" assert data["priority"] == "high" assert data["status"] == "open" async def test_create_task_with_due_date(self, client: AsyncClient, db_session): """POST /tasks with due_date stores it correctly.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") resp = await client.post( "/api/v1/tasks", json={"title": "Follow up", "due_date": "2025-12-31T10:00:00Z"}, headers=ORIGIN_HEADER, ) assert resp.status_code == 201 assert resp.json()["due_date"] is not None async def test_create_task_empty_title_returns_422(self, client: AsyncClient, db_session): """POST /tasks with empty title returns 422.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") resp = await client.post( "/api/v1/tasks", json={"title": ""}, headers=ORIGIN_HEADER, ) assert resp.status_code == 422 @pytest.mark.asyncio class TestTaskUpdate: """PATCH /api/v1/tasks/{id}""" async def test_update_task_returns_200(self, client: AsyncClient, db_session): """PATCH /tasks/{id} updates the task.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") # Create create_resp = await client.post( "/api/v1/tasks", json={"title": "Original"}, headers=ORIGIN_HEADER, ) task_id = create_resp.json()["id"] # Update resp = await client.patch( f"/api/v1/tasks/{task_id}", json={"title": "Updated", "status": "in_progress"}, headers=ORIGIN_HEADER, ) assert resp.status_code == 200 assert resp.json()["title"] == "Updated" assert resp.json()["status"] == "in_progress" async def test_update_task_not_found_returns_404(self, client: AsyncClient, db_session): """PATCH non-existent task returns 404.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") resp = await client.patch( "/api/v1/tasks/00000000-0000-0000-0000-000000000000", json={"title": "Updated"}, headers=ORIGIN_HEADER, ) assert resp.status_code == 404 @pytest.mark.asyncio class TestTaskStatus: """POST /api/v1/tasks/{id}/status""" async def test_update_status_returns_200(self, client: AsyncClient, db_session): """POST /tasks/{id}/status updates status.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") create_resp = await client.post( "/api/v1/tasks", json={"title": "Task to complete"}, headers=ORIGIN_HEADER, ) task_id = create_resp.json()["id"] resp = await client.post( f"/api/v1/tasks/{task_id}/status", json={"status": "done"}, headers=ORIGIN_HEADER, ) assert resp.status_code == 200 assert resp.json()["status"] == "done" async def test_update_status_invalid_returns_422(self, client: AsyncClient, db_session): """POST /tasks/{id}/status with invalid status returns 422.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") create_resp = await client.post( "/api/v1/tasks", json={"title": "Task"}, headers=ORIGIN_HEADER, ) task_id = create_resp.json()["id"] resp = await client.post( f"/api/v1/tasks/{task_id}/status", json={"status": "invalid"}, headers=ORIGIN_HEADER, ) assert resp.status_code == 422 @pytest.mark.asyncio class TestTaskDelete: """DELETE /api/v1/tasks/{id}""" async def test_delete_task_returns_204(self, client: AsyncClient, db_session): """DELETE /tasks/{id} soft-deletes the task.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") create_resp = await client.post( "/api/v1/tasks", json={"title": "To delete"}, headers=ORIGIN_HEADER, ) task_id = create_resp.json()["id"] resp = await client.delete(f"/api/v1/tasks/{task_id}", headers=ORIGIN_HEADER) assert resp.status_code == 204 # Verify it's gone from list list_resp = await client.get("/api/v1/tasks", headers=ORIGIN_HEADER) assert not any(t["id"] == task_id for t in list_resp.json()["items"])