@@ -652,3 +652,93 @@ def test_list_tags_permission_denied(self, mock_zammad_api: Mock) -> None:
652652
653653 with pytest .raises (requests .HTTPError , match = "403" ):
654654 client .list_tags ()
655+
656+
657+ class TestMergeTickets :
658+ """Test ZammadClient.merge_tickets."""
659+
660+ @pytest .fixture
661+ def mock_zammad_api (self ) -> Generator [Mock , None , None ]:
662+ """Mock the underlying zammad_py.ZammadAPI."""
663+ with patch ("mcp_zammad.client.ZammadAPI" ) as mock_api :
664+ yield mock_api
665+
666+ def _make_client (self , mock_zammad_api : Mock ) -> tuple [ZammadClient , Mock ]:
667+ """Build a client with a mocked API and return (client, mock_instance)."""
668+ mock_instance = Mock ()
669+ mock_zammad_api .return_value = mock_instance
670+ client = ZammadClient (url = "https://test.zammad.com/api/v1" , http_token = "test-token" )
671+ return client , mock_instance
672+
673+ def test_merge_with_target_number (self , mock_zammad_api : Mock ) -> None :
674+ """Merge uses legacy ticket_merge route with the target number."""
675+ client , mock_instance = self ._make_client (mock_zammad_api )
676+ mock_response = Mock ()
677+ mock_response .json .return_value = {
678+ "result" : "success" ,
679+ "target_ticket" : {"id" : 124 , "number" : "65004" },
680+ "source_ticket" : {"id" : 123 , "number" : "65003" , "state_id" : 5 },
681+ }
682+ mock_response .raise_for_status = Mock ()
683+ mock_instance .session .put .return_value = mock_response
684+
685+ result = client .merge_tickets (source_ticket_id = 123 , target_ticket_number = "65004" )
686+
687+ assert result ["result" ] == "success"
688+ assert result ["target_ticket" ]["id" ] == 124
689+ mock_instance .session .put .assert_called_once_with ("https://test.zammad.com/api/v1/ticket_merge/123/65004" )
690+ mock_instance .ticket .find .assert_not_called ()
691+
692+ def test_merge_with_target_id_looks_up_number (self , mock_zammad_api : Mock ) -> None :
693+ """When only the target ID is given, its number is resolved first."""
694+ client , mock_instance = self ._make_client (mock_zammad_api )
695+ mock_instance .ticket .find .return_value = {"id" : 124 , "number" : "65004" }
696+ mock_response = Mock ()
697+ mock_response .json .return_value = {"result" : "success" , "target_ticket" : {"id" : 124 , "number" : "65004" }}
698+ mock_response .raise_for_status = Mock ()
699+ mock_instance .session .put .return_value = mock_response
700+
701+ result = client .merge_tickets (source_ticket_id = 123 , target_ticket_id = 124 )
702+
703+ mock_instance .ticket .find .assert_called_once_with (124 )
704+ mock_instance .session .put .assert_called_once_with ("https://test.zammad.com/api/v1/ticket_merge/123/65004" )
705+ assert result ["result" ] == "success"
706+
707+ def test_merge_rejects_both_targets (self , mock_zammad_api : Mock ) -> None :
708+ """Providing both target identifiers raises ValueError."""
709+ client , mock_instance = self ._make_client (mock_zammad_api )
710+
711+ with pytest .raises (ValueError , match = "exactly one" ):
712+ client .merge_tickets (source_ticket_id = 123 , target_ticket_number = "65004" , target_ticket_id = 124 )
713+
714+ mock_instance .session .put .assert_not_called ()
715+
716+ def test_merge_rejects_no_target (self , mock_zammad_api : Mock ) -> None :
717+ """Providing no target identifier raises ValueError."""
718+ client , mock_instance = self ._make_client (mock_zammad_api )
719+
720+ with pytest .raises (ValueError , match = "exactly one" ):
721+ client .merge_tickets (source_ticket_id = 123 )
722+
723+ mock_instance .session .put .assert_not_called ()
724+
725+ def test_merge_http_error_propagates (self , mock_zammad_api : Mock ) -> None :
726+ """HTTP errors from the merge endpoint propagate."""
727+ client , mock_instance = self ._make_client (mock_zammad_api )
728+ mock_response = Mock ()
729+ mock_response .raise_for_status .side_effect = requests .HTTPError ("404 Not Found" )
730+ mock_instance .session .put .return_value = mock_response
731+
732+ with pytest .raises (requests .HTTPError , match = "404" ):
733+ client .merge_tickets (source_ticket_id = 123 , target_ticket_number = "99999" )
734+
735+ def test_merge_failed_result_raises (self , mock_zammad_api : Mock ) -> None :
736+ """Zammad reports merge failures as HTTP 200 with result='failed' - must raise."""
737+ client , mock_instance = self ._make_client (mock_zammad_api )
738+ mock_response = Mock ()
739+ mock_response .json .return_value = {"result" : "failed" , "message" : "The source ticket could not be found." }
740+ mock_response .raise_for_status = Mock ()
741+ mock_instance .session .put .return_value = mock_response
742+
743+ with pytest .raises (ValueError , match = "source ticket could not be found" ):
744+ client .merge_tickets (source_ticket_id = 99999999 , target_ticket_number = "65004" )
0 commit comments