Squashed 'pm/' content from commit 91f3271

git-subtree-dir: pm
git-subtree-split: 91f3271d866fa8ee471cfb6793e334b83e6d2b6e
This commit is contained in:
MMD
2026-10-07 15:14:20 +03:30
commit 934b74ebbd
87 changed files with 23922 additions and 0 deletions
+207
View File
@@ -0,0 +1,207 @@
"""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()