"""API tests for native Projects and Tasks endpoints.""" import pytest from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker from app.db import get_db from app.main import app from app.models import Base, PMUser, Project, ProjectStatus, Task from app.security import create_token, hash_password @pytest.fixture def anyio_backend(): return "asyncio" @pytest.mark.asyncio async def test_native_project_and_task_api(): engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) # Seed admin user async with async_session() as session: admin = PMUser( username="admin", password_hash=hash_password("admin123"), display_name="مدیر ارشد", role="project_manager", ) session.add(admin) await session.commit() admin_id = admin.id token = create_token(admin_id) headers = {"Authorization": f"Bearer {token}"} async def override_get_db(): async with async_session() as s: yield s app.dependency_overrides[get_db] = override_get_db transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: # 1. Create a native project resp = await client.post( "/api/v1/projects", json={ "name": "پروژه بیمارستان", "description": "احداث فاز ۲ بیمارستان", "location": "تهران", "status": "ACTIVE", "planned_start": "1405/01/01", "planned_end": "1405/12/29", }, headers=headers, ) assert resp.status_code == 200, resp.text proj_data = resp.json()["data"] project_id = proj_data["id"] assert proj_data["name"] == "پروژه بیمارستان" assert proj_data["status"] == "ACTIVE" # 2. List projects resp = await client.get("/api/v1/projects", headers=headers) assert resp.status_code == 200 projects_list = resp.json()["data"] assert len(projects_list) == 1 assert projects_list[0]["id"] == project_id # 3. Create a root task (Phase) resp = await client.post( "/api/v1/tasks", json={ "project_id": project_id, "title": "فاز ۱: اسکلت‌بندی", "priority": "high", "planned_start": "1405/01/01", "duration": 60, }, headers=headers, ) assert resp.status_code == 200, resp.text phase_task = resp.json()["data"] phase_id = phase_task["id"] assert phase_task["title"] == "فاز ۱: اسکلت‌بندی" # 4. Create a child task (Subtask) resp = await client.post( "/api/v1/tasks", json={ "project_id": project_id, "parent_id": phase_id, "title": "فعالیت ۱.۱: آرماتوربندی ستون‌ها", "priority": "urgent", "planned_start": "1405/01/05", "duration": 15, }, headers=headers, ) assert resp.status_code == 200, resp.text subtask = resp.json()["data"] assert subtask["parent_id"] == phase_id # 5. Get Phase task detail and verify children resp = await client.get(f"/api/v1/tasks/{phase_id}", headers=headers) assert resp.status_code == 200 detail = resp.json()["data"] assert len(detail["children"]) == 1 assert detail["children"][0]["id"] == subtask["id"] # 6. Update task progress with QC invariant rule # Since no QC questions exist for this task, progress can increase directly resp = await client.patch( f"/api/v1/tasks/{subtask['id']}/progress", json={"percentage": 50}, headers=headers, ) assert resp.status_code == 200 assert resp.json()["data"]["progress"] == 50 assert resp.json()["data"]["status"] == "IN_PROGRESS" # 7. Verify mandatory start/end dates: task without dates must be rejected resp_no_dates = await client.post( "/api/v1/tasks", json={ "project_id": project_id, "title": "وظیفه بدون تاریخ", }, headers=headers, ) assert resp_no_dates.status_code in (400, 422) # 8. Verify project creation with simplified fields (no code, client, dates required) resp_simple_proj = await client.post( "/api/v1/projects", json={ "name": "پروژه ساده نوساز", }, headers=headers, ) assert resp_simple_proj.status_code == 200 simple_data = resp_simple_proj.json()["data"] assert simple_data["name"] == "پروژه ساده نوساز" assert "id" in simple_data # 9. Verify task creation and update with Human Resources and Materials (including equipment) resp_resources_task = await client.post( "/api/v1/tasks", json={ "project_id": project_id, "title": "بتن‌ریزی فونداسیون با منابع کامل", "planned_start": "1405/01/10", "duration": 5, "human_resources": [ {"role": "سرکارگر بتن‌ریزی", "count": 1}, {"role": "کارگر ساده", "count": 6}, ], "materials": [ { "name": "بتن آماده عیار ۳۵۰", "unit": "متر مکعب", "specifications": ["تیپ ۲", "اسلامپ ۸"], "count": 150.5, }, { "name": "پمپ بتن دکلی", "unit": "دستگاه", "specifications": ["دکل ۳۶ متری"], "count": 1, }, ], }, headers=headers, ) assert resp_resources_task.status_code == 200 res_data = resp_resources_task.json()["data"] assert len(res_data["human_resources"]) == 2 assert res_data["human_resources"][0]["role"] == "سرکارگر بتن‌ریزی" assert len(res_data["materials"]) == 2 assert res_data["materials"][0]["unit"] == "متر مکعب" assert res_data["materials"][0]["specifications"] == ["تیپ ۲", "اسلامپ ۸"] assert res_data["materials"][1]["name"] == "پمپ بتن دکلی" # Update resources via PATCH task_id = res_data["id"] patch_resp = await client.patch( f"/api/v1/tasks/{task_id}", json={ "human_resources": [ {"role": "سرکارگر بتن‌ریزی", "count": 2}, ], }, headers=headers, ) assert patch_resp.status_code == 200 patched_data = patch_resp.json()["data"] assert len(patched_data["human_resources"]) == 1 assert patched_data["human_resources"][0]["count"] == 2 app.dependency_overrides.clear() await engine.dispose()