BE-PR-001: Update tests for Propose -> Proposal rename
- Tests now use /proposals canonical URL - Added legacy /proposes backward-compat tests - Updated class/function names to Proposal terminology
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
|||||||
"""P13.3 — Propose backend tests.
|
"""Proposal backend tests (renamed from Propose).
|
||||||
|
|
||||||
Covers:
|
Covers:
|
||||||
- CRUD: create, list, get, update
|
- CRUD: create, list, get, update
|
||||||
@@ -8,8 +8,9 @@ Covers:
|
|||||||
- reject → status change
|
- reject → status change
|
||||||
- rejected → reopen back to open
|
- rejected → reopen back to open
|
||||||
- feat_task_id cannot be set manually
|
- feat_task_id cannot be set manually
|
||||||
- edit restrictions (only open proposes editable)
|
- edit restrictions (only open proposals editable)
|
||||||
- permission checks for accept/reject/reopen
|
- permission checks for accept/reject/reopen
|
||||||
|
- Legacy /proposes endpoint still works
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
from app.models.milestone import MilestoneStatus
|
from app.models.milestone import MilestoneStatus
|
||||||
@@ -20,7 +21,14 @@ from app.models.task import TaskStatus
|
|||||||
# Helpers
|
# Helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _propose_url(project_id: int, propose_id: int | None = None) -> str:
|
def _proposal_url(project_id: int, proposal_id: int | None = None) -> str:
|
||||||
|
"""Canonical /proposals URL."""
|
||||||
|
base = f"/projects/{project_id}/proposals"
|
||||||
|
return f"{base}/{proposal_id}" if proposal_id else base
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_propose_url(project_id: int, propose_id: int | None = None) -> str:
|
||||||
|
"""Legacy /proposes URL for backward-compat tests."""
|
||||||
base = f"/projects/{project_id}/proposes"
|
base = f"/projects/{project_id}/proposes"
|
||||||
return f"{base}/{propose_id}" if propose_id else base
|
return f"{base}/{propose_id}" if propose_id else base
|
||||||
|
|
||||||
@@ -29,10 +37,10 @@ def _propose_url(project_id: int, propose_id: int | None = None) -> str:
|
|||||||
# CRUD
|
# CRUD
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
class TestProposeCRUD:
|
class TestProposalCRUD:
|
||||||
"""Basic create / list / get / update."""
|
"""Basic create / list / get / update."""
|
||||||
|
|
||||||
def test_create_propose(
|
def test_create_proposal(
|
||||||
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
||||||
):
|
):
|
||||||
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
||||||
@@ -41,7 +49,7 @@ class TestProposeCRUD:
|
|||||||
make_member(project.id, user.id, dev_role.id)
|
make_member(project.id, user.id, dev_role.id)
|
||||||
|
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
_propose_url(project.id),
|
_proposal_url(project.id),
|
||||||
json={"title": "New Feature Idea", "description": "Some details"},
|
json={"title": "New Feature Idea", "description": "Some details"},
|
||||||
headers=auth_header(user),
|
headers=auth_header(user),
|
||||||
)
|
)
|
||||||
@@ -52,7 +60,7 @@ class TestProposeCRUD:
|
|||||||
assert data["propose_code"].startswith("PROJ:P")
|
assert data["propose_code"].startswith("PROJ:P")
|
||||||
assert data["feat_task_id"] is None
|
assert data["feat_task_id"] is None
|
||||||
|
|
||||||
def test_list_proposes(
|
def test_list_proposals(
|
||||||
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
||||||
):
|
):
|
||||||
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
||||||
@@ -60,15 +68,15 @@ class TestProposeCRUD:
|
|||||||
project = make_project(owner_id=user.id)
|
project = make_project(owner_id=user.id)
|
||||||
make_member(project.id, user.id, dev_role.id)
|
make_member(project.id, user.id, dev_role.id)
|
||||||
|
|
||||||
# Create two proposes
|
# Create two proposals
|
||||||
client.post(_propose_url(project.id), json={"title": "P1"}, headers=auth_header(user))
|
client.post(_proposal_url(project.id), json={"title": "P1"}, headers=auth_header(user))
|
||||||
client.post(_propose_url(project.id), json={"title": "P2"}, headers=auth_header(user))
|
client.post(_proposal_url(project.id), json={"title": "P2"}, headers=auth_header(user))
|
||||||
|
|
||||||
resp = client.get(_propose_url(project.id), headers=auth_header(user))
|
resp = client.get(_proposal_url(project.id), headers=auth_header(user))
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert len(resp.json()) == 2
|
assert len(resp.json()) == 2
|
||||||
|
|
||||||
def test_get_propose(
|
def test_get_proposal(
|
||||||
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
||||||
):
|
):
|
||||||
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
||||||
@@ -76,14 +84,14 @@ class TestProposeCRUD:
|
|||||||
project = make_project(owner_id=user.id)
|
project = make_project(owner_id=user.id)
|
||||||
make_member(project.id, user.id, dev_role.id)
|
make_member(project.id, user.id, dev_role.id)
|
||||||
|
|
||||||
create_resp = client.post(_propose_url(project.id), json={"title": "P1"}, headers=auth_header(user))
|
create_resp = client.post(_proposal_url(project.id), json={"title": "P1"}, headers=auth_header(user))
|
||||||
propose_id = create_resp.json()["id"]
|
proposal_id = create_resp.json()["id"]
|
||||||
|
|
||||||
resp = client.get(_propose_url(project.id, propose_id), headers=auth_header(user))
|
resp = client.get(_proposal_url(project.id, proposal_id), headers=auth_header(user))
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.json()["title"] == "P1"
|
assert resp.json()["title"] == "P1"
|
||||||
|
|
||||||
def test_update_propose_open(
|
def test_update_proposal_open(
|
||||||
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
||||||
):
|
):
|
||||||
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
||||||
@@ -91,11 +99,11 @@ class TestProposeCRUD:
|
|||||||
project = make_project(owner_id=user.id)
|
project = make_project(owner_id=user.id)
|
||||||
make_member(project.id, user.id, dev_role.id)
|
make_member(project.id, user.id, dev_role.id)
|
||||||
|
|
||||||
create_resp = client.post(_propose_url(project.id), json={"title": "Old"}, headers=auth_header(user))
|
create_resp = client.post(_proposal_url(project.id), json={"title": "Old"}, headers=auth_header(user))
|
||||||
propose_id = create_resp.json()["id"]
|
proposal_id = create_resp.json()["id"]
|
||||||
|
|
||||||
resp = client.patch(
|
resp = client.patch(
|
||||||
_propose_url(project.id, propose_id),
|
_proposal_url(project.id, proposal_id),
|
||||||
json={"title": "New Title", "description": "Updated"},
|
json={"title": "New Title", "description": "Updated"},
|
||||||
headers=auth_header(user),
|
headers=auth_header(user),
|
||||||
)
|
)
|
||||||
@@ -105,11 +113,11 @@ class TestProposeCRUD:
|
|||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Propose Code
|
# Proposal Code
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
class TestProposeCode:
|
class TestProposalCode:
|
||||||
"""P1.4 — propose_code increments per project independently."""
|
"""propose_code increments per project independently."""
|
||||||
|
|
||||||
def test_code_increments_per_project(
|
def test_code_increments_per_project(
|
||||||
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
||||||
@@ -122,11 +130,11 @@ class TestProposeCode:
|
|||||||
make_member(proj_b.id, user.id, dev_role.id)
|
make_member(proj_b.id, user.id, dev_role.id)
|
||||||
|
|
||||||
# Create 2 in ALPHA
|
# Create 2 in ALPHA
|
||||||
r1 = client.post(_propose_url(proj_a.id), json={"title": "A1"}, headers=auth_header(user))
|
r1 = client.post(_proposal_url(proj_a.id), json={"title": "A1"}, headers=auth_header(user))
|
||||||
r2 = client.post(_propose_url(proj_a.id), json={"title": "A2"}, headers=auth_header(user))
|
r2 = client.post(_proposal_url(proj_a.id), json={"title": "A2"}, headers=auth_header(user))
|
||||||
|
|
||||||
# Create 1 in BETA
|
# Create 1 in BETA
|
||||||
r3 = client.post(_propose_url(proj_b.id), json={"title": "B1"}, headers=auth_header(user))
|
r3 = client.post(_proposal_url(proj_b.id), json={"title": "B1"}, headers=auth_header(user))
|
||||||
|
|
||||||
code1 = r1.json()["propose_code"]
|
code1 = r1.json()["propose_code"]
|
||||||
code2 = r2.json()["propose_code"]
|
code2 = r2.json()["propose_code"]
|
||||||
@@ -144,7 +152,7 @@ class TestProposeCode:
|
|||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
class TestAccept:
|
class TestAccept:
|
||||||
"""P6.2 — accept propose → create feature story task."""
|
"""accept proposal → create feature story task."""
|
||||||
|
|
||||||
def test_accept_success(
|
def test_accept_success(
|
||||||
self, client, db, make_user, make_project, make_milestone, seed_roles_and_permissions, make_member, auth_header,
|
self, client, db, make_user, make_project, make_milestone, seed_roles_and_permissions, make_member, auth_header,
|
||||||
@@ -157,14 +165,14 @@ class TestAccept:
|
|||||||
ms = make_milestone(project.id, mgr.id, status=MilestoneStatus.OPEN)
|
ms = make_milestone(project.id, mgr.id, status=MilestoneStatus.OPEN)
|
||||||
|
|
||||||
create_resp = client.post(
|
create_resp = client.post(
|
||||||
_propose_url(project.id),
|
_proposal_url(project.id),
|
||||||
json={"title": "Cool Feature", "description": "Do something cool"},
|
json={"title": "Cool Feature", "description": "Do something cool"},
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
propose_id = create_resp.json()["id"]
|
proposal_id = create_resp.json()["id"]
|
||||||
|
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
_propose_url(project.id, propose_id) + "/accept",
|
_proposal_url(project.id, proposal_id) + "/accept",
|
||||||
json={"milestone_id": ms.id},
|
json={"milestone_id": ms.id},
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
@@ -196,14 +204,14 @@ class TestAccept:
|
|||||||
ms = make_milestone(project.id, mgr.id, status=MilestoneStatus.FREEZE)
|
ms = make_milestone(project.id, mgr.id, status=MilestoneStatus.FREEZE)
|
||||||
|
|
||||||
create_resp = client.post(
|
create_resp = client.post(
|
||||||
_propose_url(project.id),
|
_proposal_url(project.id),
|
||||||
json={"title": "Feature X"},
|
json={"title": "Feature X"},
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
propose_id = create_resp.json()["id"]
|
proposal_id = create_resp.json()["id"]
|
||||||
|
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
_propose_url(project.id, propose_id) + "/accept",
|
_proposal_url(project.id, proposal_id) + "/accept",
|
||||||
json={"milestone_id": ms.id},
|
json={"milestone_id": ms.id},
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
@@ -221,20 +229,20 @@ class TestAccept:
|
|||||||
ms = make_milestone(project.id, mgr.id, status=MilestoneStatus.OPEN)
|
ms = make_milestone(project.id, mgr.id, status=MilestoneStatus.OPEN)
|
||||||
|
|
||||||
create_resp = client.post(
|
create_resp = client.post(
|
||||||
_propose_url(project.id), json={"title": "F"}, headers=auth_header(mgr),
|
_proposal_url(project.id), json={"title": "F"}, headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
propose_id = create_resp.json()["id"]
|
proposal_id = create_resp.json()["id"]
|
||||||
|
|
||||||
# First accept
|
# First accept
|
||||||
client.post(
|
client.post(
|
||||||
_propose_url(project.id, propose_id) + "/accept",
|
_proposal_url(project.id, proposal_id) + "/accept",
|
||||||
json={"milestone_id": ms.id},
|
json={"milestone_id": ms.id},
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Second accept should fail
|
# Second accept should fail
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
_propose_url(project.id, propose_id) + "/accept",
|
_proposal_url(project.id, proposal_id) + "/accept",
|
||||||
json={"milestone_id": ms.id},
|
json={"milestone_id": ms.id},
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
@@ -251,12 +259,12 @@ class TestAccept:
|
|||||||
ms = make_milestone(project.id, mgr.id, status=MilestoneStatus.OPEN)
|
ms = make_milestone(project.id, mgr.id, status=MilestoneStatus.OPEN)
|
||||||
|
|
||||||
create_resp = client.post(
|
create_resp = client.post(
|
||||||
_propose_url(project.id), json={"title": "F"}, headers=auth_header(mgr),
|
_proposal_url(project.id), json={"title": "F"}, headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
propose_id = create_resp.json()["id"]
|
proposal_id = create_resp.json()["id"]
|
||||||
|
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
_propose_url(project.id, propose_id) + "/accept",
|
_proposal_url(project.id, proposal_id) + "/accept",
|
||||||
json={"milestone_id": ms.id},
|
json={"milestone_id": ms.id},
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
@@ -264,7 +272,7 @@ class TestAccept:
|
|||||||
assert data["feat_task_id"] is not None
|
assert data["feat_task_id"] is not None
|
||||||
|
|
||||||
# Re-fetch to confirm persistence
|
# Re-fetch to confirm persistence
|
||||||
get_resp = client.get(_propose_url(project.id, propose_id), headers=auth_header(mgr))
|
get_resp = client.get(_proposal_url(project.id, proposal_id), headers=auth_header(mgr))
|
||||||
assert get_resp.json()["feat_task_id"] == data["feat_task_id"]
|
assert get_resp.json()["feat_task_id"] == data["feat_task_id"]
|
||||||
|
|
||||||
def test_accept_no_permission_fails(
|
def test_accept_no_permission_fails(
|
||||||
@@ -280,15 +288,15 @@ class TestAccept:
|
|||||||
|
|
||||||
ms = make_milestone(project.id, owner.id, status=MilestoneStatus.OPEN)
|
ms = make_milestone(project.id, owner.id, status=MilestoneStatus.OPEN)
|
||||||
|
|
||||||
# Dev creates the propose
|
# Dev creates the proposal
|
||||||
create_resp = client.post(
|
create_resp = client.post(
|
||||||
_propose_url(project.id), json={"title": "F"}, headers=auth_header(dev_user),
|
_proposal_url(project.id), json={"title": "F"}, headers=auth_header(dev_user),
|
||||||
)
|
)
|
||||||
propose_id = create_resp.json()["id"]
|
proposal_id = create_resp.json()["id"]
|
||||||
|
|
||||||
# Dev tries to accept — should fail
|
# Dev tries to accept — should fail
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
_propose_url(project.id, propose_id) + "/accept",
|
_proposal_url(project.id, proposal_id) + "/accept",
|
||||||
json={"milestone_id": ms.id},
|
json={"milestone_id": ms.id},
|
||||||
headers=auth_header(dev_user),
|
headers=auth_header(dev_user),
|
||||||
)
|
)
|
||||||
@@ -300,7 +308,7 @@ class TestAccept:
|
|||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
class TestReject:
|
class TestReject:
|
||||||
"""P6.3 — reject propose."""
|
"""reject proposal."""
|
||||||
|
|
||||||
def test_reject_success(
|
def test_reject_success(
|
||||||
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
||||||
@@ -311,12 +319,12 @@ class TestReject:
|
|||||||
make_member(project.id, mgr.id, mgr_role.id)
|
make_member(project.id, mgr.id, mgr_role.id)
|
||||||
|
|
||||||
create_resp = client.post(
|
create_resp = client.post(
|
||||||
_propose_url(project.id), json={"title": "F"}, headers=auth_header(mgr),
|
_proposal_url(project.id), json={"title": "F"}, headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
propose_id = create_resp.json()["id"]
|
proposal_id = create_resp.json()["id"]
|
||||||
|
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
_propose_url(project.id, propose_id) + "/reject",
|
_proposal_url(project.id, proposal_id) + "/reject",
|
||||||
json={"reason": "Not needed"},
|
json={"reason": "Not needed"},
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
@@ -334,20 +342,20 @@ class TestReject:
|
|||||||
ms = make_milestone(project.id, mgr.id, status=MilestoneStatus.OPEN)
|
ms = make_milestone(project.id, mgr.id, status=MilestoneStatus.OPEN)
|
||||||
|
|
||||||
create_resp = client.post(
|
create_resp = client.post(
|
||||||
_propose_url(project.id), json={"title": "F"}, headers=auth_header(mgr),
|
_proposal_url(project.id), json={"title": "F"}, headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
propose_id = create_resp.json()["id"]
|
proposal_id = create_resp.json()["id"]
|
||||||
|
|
||||||
# Accept first
|
# Accept first
|
||||||
client.post(
|
client.post(
|
||||||
_propose_url(project.id, propose_id) + "/accept",
|
_proposal_url(project.id, proposal_id) + "/accept",
|
||||||
json={"milestone_id": ms.id},
|
json={"milestone_id": ms.id},
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Now reject should fail
|
# Now reject should fail
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
_propose_url(project.id, propose_id) + "/reject",
|
_proposal_url(project.id, proposal_id) + "/reject",
|
||||||
json={"reason": "Changed mind"},
|
json={"reason": "Changed mind"},
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
@@ -364,12 +372,12 @@ class TestReject:
|
|||||||
make_member(project.id, dev_user.id, dev_role.id)
|
make_member(project.id, dev_user.id, dev_role.id)
|
||||||
|
|
||||||
create_resp = client.post(
|
create_resp = client.post(
|
||||||
_propose_url(project.id), json={"title": "F"}, headers=auth_header(dev_user),
|
_proposal_url(project.id), json={"title": "F"}, headers=auth_header(dev_user),
|
||||||
)
|
)
|
||||||
propose_id = create_resp.json()["id"]
|
proposal_id = create_resp.json()["id"]
|
||||||
|
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
_propose_url(project.id, propose_id) + "/reject",
|
_proposal_url(project.id, proposal_id) + "/reject",
|
||||||
json={"reason": "nah"},
|
json={"reason": "nah"},
|
||||||
headers=auth_header(dev_user),
|
headers=auth_header(dev_user),
|
||||||
)
|
)
|
||||||
@@ -381,7 +389,7 @@ class TestReject:
|
|||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
class TestReopen:
|
class TestReopen:
|
||||||
"""P6.4 — reopen rejected propose."""
|
"""reopen rejected proposal."""
|
||||||
|
|
||||||
def test_reopen_success(
|
def test_reopen_success(
|
||||||
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
||||||
@@ -392,20 +400,20 @@ class TestReopen:
|
|||||||
make_member(project.id, mgr.id, mgr_role.id)
|
make_member(project.id, mgr.id, mgr_role.id)
|
||||||
|
|
||||||
create_resp = client.post(
|
create_resp = client.post(
|
||||||
_propose_url(project.id), json={"title": "F"}, headers=auth_header(mgr),
|
_proposal_url(project.id), json={"title": "F"}, headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
propose_id = create_resp.json()["id"]
|
proposal_id = create_resp.json()["id"]
|
||||||
|
|
||||||
# Reject first
|
# Reject first
|
||||||
client.post(
|
client.post(
|
||||||
_propose_url(project.id, propose_id) + "/reject",
|
_proposal_url(project.id, proposal_id) + "/reject",
|
||||||
json={"reason": "wait"},
|
json={"reason": "wait"},
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Reopen
|
# Reopen
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
_propose_url(project.id, propose_id) + "/reopen",
|
_proposal_url(project.id, proposal_id) + "/reopen",
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
@@ -420,13 +428,13 @@ class TestReopen:
|
|||||||
make_member(project.id, mgr.id, mgr_role.id)
|
make_member(project.id, mgr.id, mgr_role.id)
|
||||||
|
|
||||||
create_resp = client.post(
|
create_resp = client.post(
|
||||||
_propose_url(project.id), json={"title": "F"}, headers=auth_header(mgr),
|
_proposal_url(project.id), json={"title": "F"}, headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
propose_id = create_resp.json()["id"]
|
proposal_id = create_resp.json()["id"]
|
||||||
|
|
||||||
# Try reopen on open propose — should fail
|
# Try reopen on open proposal — should fail
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
_propose_url(project.id, propose_id) + "/reopen",
|
_proposal_url(project.id, proposal_id) + "/reopen",
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
assert resp.status_code == 400
|
assert resp.status_code == 400
|
||||||
@@ -442,20 +450,20 @@ class TestReopen:
|
|||||||
make_member(project.id, dev_user.id, dev_role.id)
|
make_member(project.id, dev_user.id, dev_role.id)
|
||||||
|
|
||||||
create_resp = client.post(
|
create_resp = client.post(
|
||||||
_propose_url(project.id), json={"title": "F"}, headers=auth_header(dev_user),
|
_proposal_url(project.id), json={"title": "F"}, headers=auth_header(dev_user),
|
||||||
)
|
)
|
||||||
propose_id = create_resp.json()["id"]
|
proposal_id = create_resp.json()["id"]
|
||||||
|
|
||||||
# Owner rejects
|
# Owner rejects
|
||||||
client.post(
|
client.post(
|
||||||
_propose_url(project.id, propose_id) + "/reject",
|
_proposal_url(project.id, proposal_id) + "/reject",
|
||||||
json={"reason": "nah"},
|
json={"reason": "nah"},
|
||||||
headers=auth_header(owner),
|
headers=auth_header(owner),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Dev tries to reopen — should fail
|
# Dev tries to reopen — should fail
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
_propose_url(project.id, propose_id) + "/reopen",
|
_proposal_url(project.id, proposal_id) + "/reopen",
|
||||||
headers=auth_header(dev_user),
|
headers=auth_header(dev_user),
|
||||||
)
|
)
|
||||||
assert resp.status_code == 403
|
assert resp.status_code == 403
|
||||||
@@ -466,7 +474,7 @@ class TestReopen:
|
|||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
class TestFeatTaskIdProtection:
|
class TestFeatTaskIdProtection:
|
||||||
"""P6.5 — feat_task_id is server-side only, cannot be set by client."""
|
"""feat_task_id is server-side only, cannot be set by client."""
|
||||||
|
|
||||||
def test_update_cannot_set_feat_task_id(
|
def test_update_cannot_set_feat_task_id(
|
||||||
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
||||||
@@ -477,13 +485,13 @@ class TestFeatTaskIdProtection:
|
|||||||
make_member(project.id, user.id, dev_role.id)
|
make_member(project.id, user.id, dev_role.id)
|
||||||
|
|
||||||
create_resp = client.post(
|
create_resp = client.post(
|
||||||
_propose_url(project.id), json={"title": "F"}, headers=auth_header(user),
|
_proposal_url(project.id), json={"title": "F"}, headers=auth_header(user),
|
||||||
)
|
)
|
||||||
propose_id = create_resp.json()["id"]
|
proposal_id = create_resp.json()["id"]
|
||||||
|
|
||||||
# Try to set feat_task_id via PATCH
|
# Try to set feat_task_id via PATCH
|
||||||
resp = client.patch(
|
resp = client.patch(
|
||||||
_propose_url(project.id, propose_id),
|
_proposal_url(project.id, proposal_id),
|
||||||
json={"feat_task_id": "999"},
|
json={"feat_task_id": "999"},
|
||||||
headers=auth_header(user),
|
headers=auth_header(user),
|
||||||
)
|
)
|
||||||
@@ -497,9 +505,9 @@ class TestFeatTaskIdProtection:
|
|||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
class TestEditRestrictions:
|
class TestEditRestrictions:
|
||||||
"""Propose editing is only allowed in open status."""
|
"""Proposal editing is only allowed in open status."""
|
||||||
|
|
||||||
def test_edit_accepted_propose_fails(
|
def test_edit_accepted_proposal_fails(
|
||||||
self, client, db, make_user, make_project, make_milestone, seed_roles_and_permissions, make_member, auth_header,
|
self, client, db, make_user, make_project, make_milestone, seed_roles_and_permissions, make_member, auth_header,
|
||||||
):
|
):
|
||||||
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
||||||
@@ -510,27 +518,27 @@ class TestEditRestrictions:
|
|||||||
ms = make_milestone(project.id, mgr.id, status=MilestoneStatus.OPEN)
|
ms = make_milestone(project.id, mgr.id, status=MilestoneStatus.OPEN)
|
||||||
|
|
||||||
create_resp = client.post(
|
create_resp = client.post(
|
||||||
_propose_url(project.id), json={"title": "F"}, headers=auth_header(mgr),
|
_proposal_url(project.id), json={"title": "F"}, headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
propose_id = create_resp.json()["id"]
|
proposal_id = create_resp.json()["id"]
|
||||||
|
|
||||||
# Accept
|
# Accept
|
||||||
client.post(
|
client.post(
|
||||||
_propose_url(project.id, propose_id) + "/accept",
|
_proposal_url(project.id, proposal_id) + "/accept",
|
||||||
json={"milestone_id": ms.id},
|
json={"milestone_id": ms.id},
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Try to edit
|
# Try to edit
|
||||||
resp = client.patch(
|
resp = client.patch(
|
||||||
_propose_url(project.id, propose_id),
|
_proposal_url(project.id, proposal_id),
|
||||||
json={"title": "Changed"},
|
json={"title": "Changed"},
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
assert resp.status_code == 400
|
assert resp.status_code == 400
|
||||||
assert "open" in resp.json()["detail"].lower()
|
assert "open" in resp.json()["detail"].lower()
|
||||||
|
|
||||||
def test_edit_rejected_propose_fails(
|
def test_edit_rejected_proposal_fails(
|
||||||
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
||||||
):
|
):
|
||||||
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
||||||
@@ -539,21 +547,79 @@ class TestEditRestrictions:
|
|||||||
make_member(project.id, mgr.id, mgr_role.id)
|
make_member(project.id, mgr.id, mgr_role.id)
|
||||||
|
|
||||||
create_resp = client.post(
|
create_resp = client.post(
|
||||||
_propose_url(project.id), json={"title": "F"}, headers=auth_header(mgr),
|
_proposal_url(project.id), json={"title": "F"}, headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
propose_id = create_resp.json()["id"]
|
proposal_id = create_resp.json()["id"]
|
||||||
|
|
||||||
# Reject
|
# Reject
|
||||||
client.post(
|
client.post(
|
||||||
_propose_url(project.id, propose_id) + "/reject",
|
_proposal_url(project.id, proposal_id) + "/reject",
|
||||||
json={"reason": "no"},
|
json={"reason": "no"},
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Try to edit
|
# Try to edit
|
||||||
resp = client.patch(
|
resp = client.patch(
|
||||||
_propose_url(project.id, propose_id),
|
_proposal_url(project.id, proposal_id),
|
||||||
json={"title": "Changed"},
|
json={"title": "Changed"},
|
||||||
headers=auth_header(mgr),
|
headers=auth_header(mgr),
|
||||||
)
|
)
|
||||||
assert resp.status_code == 400
|
assert resp.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# Legacy /proposes endpoint backward compatibility
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
class TestLegacyProposeEndpoint:
|
||||||
|
"""Verify the old /proposes URL still works."""
|
||||||
|
|
||||||
|
def test_legacy_create_and_list(
|
||||||
|
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
||||||
|
):
|
||||||
|
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
||||||
|
user = make_user()
|
||||||
|
project = make_project(owner_id=user.id, project_code="LEG")
|
||||||
|
make_member(project.id, user.id, dev_role.id)
|
||||||
|
|
||||||
|
# Create via legacy endpoint
|
||||||
|
resp = client.post(
|
||||||
|
_legacy_propose_url(project.id),
|
||||||
|
json={"title": "Legacy Proposal"},
|
||||||
|
headers=auth_header(user),
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
assert resp.json()["title"] == "Legacy Proposal"
|
||||||
|
|
||||||
|
# List via legacy endpoint
|
||||||
|
resp = client.get(_legacy_propose_url(project.id), headers=auth_header(user))
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert len(resp.json()) >= 1
|
||||||
|
|
||||||
|
def test_legacy_get_and_update(
|
||||||
|
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
|
||||||
|
):
|
||||||
|
admin_role, mgr_role, dev_role = seed_roles_and_permissions
|
||||||
|
user = make_user()
|
||||||
|
project = make_project(owner_id=user.id)
|
||||||
|
make_member(project.id, user.id, dev_role.id)
|
||||||
|
|
||||||
|
# Create via new endpoint
|
||||||
|
create_resp = client.post(
|
||||||
|
_proposal_url(project.id), json={"title": "Cross"}, headers=auth_header(user),
|
||||||
|
)
|
||||||
|
pid = create_resp.json()["id"]
|
||||||
|
|
||||||
|
# Get via legacy
|
||||||
|
resp = client.get(_legacy_propose_url(project.id, pid), headers=auth_header(user))
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["title"] == "Cross"
|
||||||
|
|
||||||
|
# Update via legacy
|
||||||
|
resp = client.patch(
|
||||||
|
_legacy_propose_url(project.id, pid),
|
||||||
|
json={"title": "Updated Cross"},
|
||||||
|
headers=auth_header(user),
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["title"] == "Updated Cross"
|
||||||
|
|||||||
Reference in New Issue
Block a user