diff --git a/routes/taiga_routes.py b/routes/taiga_routes.py index e8e5ca5..56de6c9 100644 --- a/routes/taiga_routes.py +++ b/routes/taiga_routes.py @@ -72,6 +72,15 @@ def taiga_webhook(): collection_name, delete_key, id, result.deleted_count ) + # When a user story is deleted, also remove all its associated tasks + if event_type == "userstory": + tasks_coll = get_collection(f"taiga_{prj}.tasks") + task_result = tasks_coll.delete_many({"userstory_id": id}) + logger.info( + "Cascade-deleted %s task(s) linked to userstory_id=%s in taiga_%s.tasks", + task_result.deleted_count, id, prj, + ) + author_login = raw_payload.get("by", {}).get("username", "unknown") logger.info( "Notifying LD_EVAL about deleted event: %s for team with external_id: %s with quality_model: %s", @@ -81,6 +90,9 @@ def taiga_webhook(): ) try: notify_eval_push(event_type, prj, author_login, quality_model) + # Force a task re-eval when tasks were cascade-deleted with the user story + if event_type == "userstory": + notify_eval_push("task", prj, author_login, quality_model) except Exception as e: logger.error("Error notifying LD_EVAL: %s", e) return jsonify({"error": "Failed to notify LD_EVAL"}), 500 diff --git a/tests/test_taiga_routes.py b/tests/test_taiga_routes.py index d0b67d2..15486fd 100644 --- a/tests/test_taiga_routes.py +++ b/tests/test_taiga_routes.py @@ -105,6 +105,43 @@ def test_delete_action(self, mock_verify, mock_coll, mock_notify, client): mock_collection.delete_one.assert_called_once_with({"task_id": 99}) mock_notify.assert_called_once_with("task", "P", "u", None) + @patch("routes.taiga_routes.notify_eval_push") + @patch("routes.taiga_routes.get_collection") + @patch("routes.taiga_routes.verify_taiga_signature", return_value=True) + def test_delete_userstory_cascades_tasks( + self, mock_verify, mock_coll, mock_notify, client + ): + payload = { + "type": "userstory", + "action": "delete", + "data": {"id": 55, "project": {"name": "P"}}, + "by": {"username": "u"}, + } + mock_us_collection = MagicMock() + mock_tasks_collection = MagicMock() + + def side_effect(name): + if name == "taiga_P.userstories": + return mock_us_collection + if name == "taiga_P.tasks": + return mock_tasks_collection + return MagicMock() + + mock_coll.side_effect = side_effect + + resp = client.post( + "/webhook/taiga?prj=P", + data=json.dumps(payload), + content_type="application/json", + headers={"X-TAIGA-WEBHOOK-SIGNATURE": "x"}, + ) + assert resp.status_code == 200 + mock_us_collection.delete_one.assert_called_once_with({"userstory_id": 55}) + mock_tasks_collection.delete_many.assert_called_once_with({"userstory_id": 55}) + assert mock_notify.call_count == 2 + mock_notify.assert_any_call("userstory", "P", "u", None) + mock_notify.assert_any_call("task", "P", "u", None) + @patch("routes.taiga_routes.get_collection") @patch("routes.taiga_routes.verify_taiga_signature", return_value=True) def test_delete_no_id_returns_400(self, mock_verify, mock_coll, client):