208 lines
7.6 KiB
Python
208 lines
7.6 KiB
Python
"""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()
|