@@ -1425,3 +1425,155 @@ def test_risk_acceptance_download_proof_writer_allowed(self):
14251425
14261426 # Clean up uploaded file
14271427 self .risk_acceptance .path .delete (save = True )
1428+
1429+
1430+ class TestEngagementMovePermission (DojoTestCase ):
1431+
1432+ """Moving an engagement to another product requires Engagement_Edit on the destination."""
1433+
1434+ @classmethod
1435+ def setUpTestData (cls ):
1436+ cls .owner_role = Role .objects .get (name = "Owner" )
1437+ cls .product_type = Product_Type .objects .create (name = "Eng Move Test PT" )
1438+
1439+ cls .product_a = Product .objects .create (
1440+ name = "Eng Move Product A" , description = "Test" , prod_type = cls .product_type ,
1441+ )
1442+ cls .product_b = Product .objects .create (
1443+ name = "Eng Move Product B" , description = "Test" , prod_type = cls .product_type ,
1444+ )
1445+ cls .product_c = Product .objects .create (
1446+ name = "Eng Move Product C" , description = "Test" , prod_type = cls .product_type ,
1447+ )
1448+
1449+ cls .user = Dojo_User .objects .create_user (
1450+ username = "eng_move_owner" ,
1451+ password = "testTEST1234!@#$" , # noqa: S106
1452+ is_active = True ,
1453+ )
1454+ Product_Member .objects .create (product = cls .product_a , user = cls .user , role = cls .owner_role )
1455+ # No membership on product_b -- user cannot move engagements there
1456+ Product_Member .objects .create (product = cls .product_c , user = cls .user , role = cls .owner_role )
1457+
1458+ def setUp (self ):
1459+ self .engagement = Engagement .objects .create (
1460+ name = "Move Test Engagement" ,
1461+ product = self .product_a ,
1462+ target_start = datetime .date .today (),
1463+ target_end = datetime .date .today (),
1464+ )
1465+
1466+ def _api_client (self ):
1467+ token , _ = Token .objects .get_or_create (user = self .user )
1468+ client = APIClient ()
1469+ client .credentials (HTTP_AUTHORIZATION = "Token " + token .key )
1470+ return client
1471+
1472+ def _ui_client (self ):
1473+ client = Client ()
1474+ client .login (username = "eng_move_owner" , password = "testTEST1234!@#$" ) # noqa: S106
1475+ return client
1476+
1477+ # ── API: PATCH ────────────────────────────────────────────────────
1478+
1479+ def test_api_patch_move_to_authorized_product (self ):
1480+ """PATCH with product the user has access to should succeed."""
1481+ client = self ._api_client ()
1482+ url = reverse ("engagement-detail" , args = (self .engagement .id ,))
1483+ response = client .patch (url , {"product" : self .product_c .id }, format = "json" )
1484+ self .assertEqual (response .status_code , 200 , response .content )
1485+ self .engagement .refresh_from_db ()
1486+ self .assertEqual (self .engagement .product , self .product_c )
1487+
1488+ def test_api_patch_move_to_unauthorized_product (self ):
1489+ """PATCH with product the user lacks access to should be denied."""
1490+ client = self ._api_client ()
1491+ url = reverse ("engagement-detail" , args = (self .engagement .id ,))
1492+ response = client .patch (url , {"product" : self .product_b .id }, format = "json" )
1493+ self .assertEqual (response .status_code , 403 , response .content )
1494+ self .engagement .refresh_from_db ()
1495+ self .assertEqual (self .engagement .product , self .product_a )
1496+
1497+ def test_api_patch_same_product (self ):
1498+ """PATCH with the same product should succeed without extra permission check."""
1499+ client = self ._api_client ()
1500+ url = reverse ("engagement-detail" , args = (self .engagement .id ,))
1501+ response = client .patch (url , {"product" : self .product_a .id }, format = "json" )
1502+ self .assertEqual (response .status_code , 200 , response .content )
1503+
1504+ def test_api_patch_without_product_field (self ):
1505+ """PATCH without product field should succeed (no spurious check)."""
1506+ client = self ._api_client ()
1507+ url = reverse ("engagement-detail" , args = (self .engagement .id ,))
1508+ response = client .patch (url , {"version" : "1.0" }, format = "json" )
1509+ self .assertEqual (response .status_code , 200 , response .content )
1510+
1511+ # ── API: PUT ──────────────────────────────────────────────────────
1512+
1513+ def test_api_put_move_to_authorized_product (self ):
1514+ """PUT with product the user has access to should succeed."""
1515+ client = self ._api_client ()
1516+ url = reverse ("engagement-detail" , args = (self .engagement .id ,))
1517+ payload = {
1518+ "name" : "Move Test Engagement" ,
1519+ "product" : self .product_c .id ,
1520+ "target_start" : str (datetime .date .today ()),
1521+ "target_end" : str (datetime .date .today ()),
1522+ "engagement_type" : "Interactive" ,
1523+ "status" : "Not Started" ,
1524+ }
1525+ response = client .put (url , payload , format = "json" )
1526+ self .assertEqual (response .status_code , 200 , response .content )
1527+ self .engagement .refresh_from_db ()
1528+ self .assertEqual (self .engagement .product , self .product_c )
1529+
1530+ def test_api_put_move_to_unauthorized_product (self ):
1531+ """PUT with product the user lacks access to should be denied."""
1532+ client = self ._api_client ()
1533+ url = reverse ("engagement-detail" , args = (self .engagement .id ,))
1534+ payload = {
1535+ "name" : "Move Test Engagement" ,
1536+ "product" : self .product_b .id ,
1537+ "target_start" : str (datetime .date .today ()),
1538+ "target_end" : str (datetime .date .today ()),
1539+ "engagement_type" : "Interactive" ,
1540+ "status" : "Not Started" ,
1541+ }
1542+ response = client .put (url , payload , format = "json" )
1543+ self .assertEqual (response .status_code , 403 , response .content )
1544+ self .engagement .refresh_from_db ()
1545+ self .assertEqual (self .engagement .product , self .product_a )
1546+
1547+ # ── UI ────────────────────────────────────────────────────────────
1548+
1549+ def test_ui_move_to_authorized_product (self ):
1550+ """Edit engagement form moving to authorized product should succeed."""
1551+ client = self ._ui_client ()
1552+ url = reverse ("edit_engagement" , args = (self .engagement .id ,))
1553+ form_data = {
1554+ "product" : self .product_c .id ,
1555+ "target_start" : datetime .date .today ().strftime ("%Y-%m-%d" ),
1556+ "target_end" : datetime .date .today ().strftime ("%Y-%m-%d" ),
1557+ "lead" : self .user .id ,
1558+ "status" : "Not Started" ,
1559+ }
1560+ response = client .post (url , form_data )
1561+ self .assertIn (response .status_code , [200 , 302 ], response .content [:500 ])
1562+ self .engagement .refresh_from_db ()
1563+ self .assertEqual (self .engagement .product , self .product_c )
1564+
1565+ def test_ui_move_to_unauthorized_product (self ):
1566+ """Edit engagement form moving to unauthorized product should be denied."""
1567+ client = self ._ui_client ()
1568+ url = reverse ("edit_engagement" , args = (self .engagement .id ,))
1569+ form_data = {
1570+ "product" : self .product_b .id ,
1571+ "target_start" : datetime .date .today ().strftime ("%Y-%m-%d" ),
1572+ "target_end" : datetime .date .today ().strftime ("%Y-%m-%d" ),
1573+ "lead" : self .user .id ,
1574+ "status" : "Not Started" ,
1575+ }
1576+ response = client .post (url , form_data )
1577+ self .assertEqual (response .status_code , 403 )
1578+ self .engagement .refresh_from_db ()
1579+ self .assertEqual (self .engagement .product , self .product_a )
0 commit comments