Compare commits

...

1 Commits

Author SHA1 Message Date
zhi
9e8dda3f16 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
2026-03-29 15:35:47 +00:00
4 changed files with 149 additions and 83 deletions

View File

@@ -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"