Add 134 tests as independent test project: - test_auth.py (5): Login, JWT, protected endpoints - test_users.py (8): User CRUD, permissions - test_projects.py (8): Project CRUD, ownership - test_milestones.py (7): Milestone CRUD, filtering - test_tasks.py (8): Task CRUD, filtering - test_comments.py (5): Comment CRUD, permissions - test_roles.py (9): Role/permission management - test_milestone_actions.py (17): Milestone state machine - test_task_transitions.py (34): Task state machine - test_propose.py (19): Propose CRUD, lifecycle - test_misc.py (14): Notifications, activity, API keys, dashboard Setup: - conftest.py: SQLite in-memory DB, fixtures - requirements.txt: Dependencies - pyproject.toml: Pytest config - README.md: Documentation
149 lines
5.7 KiB
Python
149 lines
5.7 KiB
Python
"""P14.1 — Milestones CRUD API tests.
|
|
|
|
Covers:
|
|
- List milestones (project-scoped)
|
|
- Get milestone by ID
|
|
- Create milestone
|
|
- Update milestone
|
|
- Delete milestone
|
|
- Milestone filtering and sorting
|
|
"""
|
|
import pytest
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
class TestMilestonesCRUD:
|
|
"""Milestone CRUD endpoints."""
|
|
|
|
def test_list_milestones(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
|
|
"""List milestones for a project."""
|
|
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
|
user = make_user()
|
|
project = make_project()
|
|
make_member(project.id, user.id, dev_role.id)
|
|
|
|
# Create milestones
|
|
from app.models.milestone import Milestone, MilestoneStatus
|
|
milestone1 = Milestone(title="Milestone 1", project_id=project.id, status=MilestoneStatus.OPEN)
|
|
milestone2 = Milestone(title="Milestone 2", project_id=project.id, status=MilestoneStatus.OPEN)
|
|
db.add_all([milestone1, milestone2])
|
|
db.commit()
|
|
|
|
resp = client.get(f"/projects/{project.id}/milestones", headers=auth_header(user))
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert len(data) == 2
|
|
|
|
def test_get_milestone_by_id(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
|
|
"""Get specific milestone."""
|
|
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
|
user = make_user()
|
|
project = make_project()
|
|
make_member(project.id, user.id, dev_role.id)
|
|
|
|
from app.models.milestone import Milestone, MilestoneStatus
|
|
milestone = Milestone(
|
|
title="Test Milestone",
|
|
description="Test desc",
|
|
project_id=project.id,
|
|
status=MilestoneStatus.OPEN
|
|
)
|
|
db.add(milestone)
|
|
db.commit()
|
|
|
|
resp = client.get(
|
|
f"/projects/{project.id}/milestones/{milestone.id}",
|
|
headers=auth_header(user)
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["id"] == milestone.id
|
|
assert data["title"] == "Test Milestone"
|
|
|
|
def test_create_milestone(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
|
|
"""Create new milestone."""
|
|
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
|
user = make_user()
|
|
project = make_project(project_code="PROJ")
|
|
make_member(project.id, user.id, dev_role.id)
|
|
|
|
due_date = (datetime.now() + timedelta(days=30)).isoformat()
|
|
resp = client.post(
|
|
f"/projects/{project.id}/milestones",
|
|
json={
|
|
"title": "New Milestone",
|
|
"description": "Milestone description",
|
|
"due_date": due_date
|
|
},
|
|
headers=auth_header(user)
|
|
)
|
|
assert resp.status_code == 201
|
|
data = resp.json()
|
|
assert data["title"] == "New Milestone"
|
|
assert data["status"] == "open"
|
|
assert data["milestone_code"].startswith("PROJ:")
|
|
|
|
def test_update_milestone(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
|
|
"""Update milestone (allowed in open status)."""
|
|
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
|
user = make_user()
|
|
project = make_project()
|
|
make_member(project.id, user.id, dev_role.id)
|
|
|
|
from app.models.milestone import Milestone, MilestoneStatus
|
|
milestone = Milestone(
|
|
title="Old Title",
|
|
project_id=project.id,
|
|
status=MilestoneStatus.OPEN
|
|
)
|
|
db.add(milestone)
|
|
db.commit()
|
|
|
|
resp = client.patch(
|
|
f"/projects/{project.id}/milestones/{milestone.id}",
|
|
json={"title": "Updated Title"},
|
|
headers=auth_header(user)
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["title"] == "Updated Title"
|
|
|
|
def test_delete_milestone(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
|
|
"""Delete milestone."""
|
|
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
|
user = make_user()
|
|
project = make_project()
|
|
make_member(project.id, user.id, dev_role.id)
|
|
|
|
from app.models.milestone import Milestone, MilestoneStatus
|
|
milestone = Milestone(title="To Delete", project_id=project.id, status=MilestoneStatus.OPEN)
|
|
db.add(milestone)
|
|
db.commit()
|
|
|
|
resp = client.delete(
|
|
f"/projects/{project.id}/milestones/{milestone.id}",
|
|
headers=auth_header(user)
|
|
)
|
|
assert resp.status_code == 204
|
|
|
|
def test_milestone_status_filter(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
|
|
"""Filter milestones by status."""
|
|
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
|
user = make_user()
|
|
project = make_project()
|
|
make_member(project.id, user.id, dev_role.id)
|
|
|
|
from app.models.milestone import Milestone, MilestoneStatus
|
|
open_ms = Milestone(title="Open", project_id=project.id, status=MilestoneStatus.OPEN)
|
|
closed_ms = Milestone(title="Closed", project_id=project.id, status=MilestoneStatus.CLOSED)
|
|
db.add_all([open_ms, closed_ms])
|
|
db.commit()
|
|
|
|
resp = client.get(
|
|
f"/projects/{project.id}/milestones?status=open",
|
|
headers=auth_header(user)
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert all(m["status"] == "open" for m in data)
|