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 from app.security import create_token, hash_password from app.utils.date_service import date_service @pytest.fixture def anyio_backend(): return "asyncio" @pytest.mark.asyncio async def test_automated_lifecycle_dates(): 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) 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 project p_res = await client.post( "/api/v1/projects", json={"name": "پروژه بیمارستان البرز"}, headers=headers, ) assert p_res.status_code == 200 project_id = p_res.json()["data"]["id"] assert p_res.json()["data"]["planned_start"] is None assert p_res.json()["data"]["planned_end"] is None # 2. Create Task 1 (earliest: 2026-10-01 to 2026-10-10) t1_res = await client.post( "/api/v1/tasks", json={ "project_id": project_id, "title": "گودبرداری و خاکبرداری", "planned_start": "2026-10-01", "planned_end": "2026-10-10", }, headers=headers, ) assert t1_res.status_code == 200 t1_id = t1_res.json()["data"]["id"] assert t1_res.json()["data"]["actual_start"] is None assert t1_res.json()["data"]["actual_end"] is None # Verify project planned dates matched Task 1 p_get1 = await client.get(f"/api/v1/projects/{project_id}", headers=headers) assert p_get1.json()["data"]["planned_start"] == "2026-10-01" assert p_get1.json()["data"]["planned_end"] == "2026-10-10" assert p_get1.json()["data"]["actual_start"] is None assert p_get1.json()["data"]["actual_end"] is None # 3. Create Task 2 (later: 2026-10-11 to 2026-10-25) t2_res = await client.post( "/api/v1/tasks", json={ "project_id": project_id, "title": "اجرای فونداسیون", "planned_start": "2026-10-11", "planned_end": "2026-10-25", }, headers=headers, ) assert t2_res.status_code == 200 t2_id = t2_res.json()["data"]["id"] # Verify project planned dates span from Task 1 (first) to Task 2 (last) p_get2 = await client.get(f"/api/v1/projects/{project_id}", headers=headers) assert p_get2.json()["data"]["planned_start"] == "2026-10-01" assert p_get2.json()["data"]["planned_end"] == "2026-10-25" # 4. Progress on Task 1 moves from 0 to 25% today_iso = date_service.today().isoformat() prog1_res = await client.patch( f"/api/v1/tasks/{t1_id}/progress", json={"percentage": 25}, headers=headers, ) assert prog1_res.status_code == 200 assert prog1_res.json()["data"]["actual_start"] == today_iso assert prog1_res.json()["data"]["actual_end"] is None # Check project dates: project.actual_start is now set, actual_end is None p_get3 = await client.get(f"/api/v1/projects/{project_id}", headers=headers) assert p_get3.json()["data"]["actual_start"] == today_iso assert p_get3.json()["data"]["actual_end"] is None # 5. Complete Task 1 (100%) prog1_100 = await client.patch( f"/api/v1/tasks/{t1_id}/progress", json={"percentage": 100}, headers=headers, ) assert prog1_100.status_code == 200 assert prog1_100.json()["data"]["actual_end"] == today_iso # Project actual_end should still be None because Task 2 (the last task) hasn't finished p_get4 = await client.get(f"/api/v1/projects/{project_id}", headers=headers) assert p_get4.json()["data"]["actual_end"] is None # 6. Complete Task 2 (last task) (100%) prog2_100 = await client.patch( f"/api/v1/tasks/{t2_id}/progress", json={"percentage": 100}, headers=headers, ) assert prog2_100.status_code == 200 assert prog2_100.json()["data"]["actual_end"] == today_iso # Now project actual_end should match Task 2's actual_end p_get5 = await client.get(f"/api/v1/projects/{project_id}", headers=headers) assert p_get5.json()["data"]["actual_end"] == today_iso assert p_get5.json()["data"]["progress"] == 100.0 # 7. Test reset to 0% on Task 2: actual_end and actual_start are cleared prog2_0 = await client.patch( f"/api/v1/tasks/{t2_id}/progress", json={"percentage": 0}, headers=headers, ) assert prog2_0.status_code == 200 assert prog2_0.json()["data"]["actual_start"] is None assert prog2_0.json()["data"]["actual_end"] is None p_get6 = await client.get(f"/api/v1/projects/{project_id}", headers=headers) assert p_get6.json()["data"]["actual_end"] is None