The PM panel (PmPanelPage -> iframe /pm/) needs the project-management platform served at /pm/ on the ERP domain. Source: org repo Aramesh/PM, branch feature/project-management-platform. Update via: git subtree pull --prefix=pm aramesh-pm feature/project-management-platform --squash
156 lines
5.9 KiB
Python
156 lines
5.9 KiB
Python
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
|