import json from unittest.mock import MagicMock, Mock, patch import pytest from openapi3 import OpenAPI from openapi3.schemas import Model from haystack.components.connectors import OpenAPIServiceConnector from haystack.dataclasses import ChatMessage @pytest.fixture def openapi_service_mock(): return MagicMock(spec=OpenAPI) class TestOpenAPIServiceConnector: @pytest.fixture def connector(self): return OpenAPIServiceConnector() def test_parse_message_invalid_json(self, connector): # Test invalid JSON content with pytest.raises(ValueError): connector._parse_message(ChatMessage.from_assistant("invalid json")) def test_parse_valid_json_message(self): connector = OpenAPIServiceConnector() # The content format here is OpenAI function calling descriptor content = ( '[{"function":{"name": "compare_branches","arguments": "{\\n \\"parameters\\": {\\n ' ' \\"basehead\\": \\"main...openapi_container_v5\\",\\n ' ' \\"owner\\": \\"deepset-ai\\",\\n \\"repo\\": \\"haystack\\"\\n }\\n}"}, "type": "function"}]' ) descriptors = connector._parse_message(ChatMessage.from_assistant(content)) # Assert that the descriptor contains the expected method name and arguments assert descriptors[0]["name"] == "compare_branches" assert descriptors[0]["arguments"]["parameters"] == { "basehead": "main...openapi_container_v5", "owner": "deepset-ai", "repo": "haystack", } # but not the requestBody assert "requestBody" not in descriptors[0]["arguments"] # The content format here is OpenAI function calling descriptor content = '[{"function": {"name": "search","arguments": "{\\n \\"requestBody\\": {\\n \\"q\\": \\"haystack\\"\\n }\\n}"}, "type": "function"}]' descriptors = connector._parse_message(ChatMessage.from_assistant(content)) assert descriptors[0]["name"] == "search" assert descriptors[0]["arguments"]["requestBody"] == {"q": "haystack"} # but not the parameters assert "parameters" not in descriptors[0]["arguments"] def test_parse_message_missing_fields(self, connector): # Test JSON content with missing fields with pytest.raises(ValueError): connector._parse_message(ChatMessage.from_assistant('[{"function": {"name": "test_method"}}]')) def test_authenticate_service_missing_authentication_token(self, connector, openapi_service_mock): securitySchemes_mock = MagicMock() securitySchemes_mock.raw_element = {"apiKey": {"in": "header", "name": "x-api-key", "type": "apiKey"}} with pytest.raises(ValueError): connector._authenticate_service(openapi_service_mock) def test_authenticate_service_having_authentication_token(self, connector, openapi_service_mock): securitySchemes_mock = MagicMock() securitySchemes_mock.raw_element = {"apiKey": {"in": "header", "name": "x-api-key", "type": "apiKey"}} openapi_service_mock.components.securitySchemes = securitySchemes_mock connector._authenticate_service(openapi_service_mock, "some_fake_token") def test_authenticate_service_having_authentication_dict(self, connector, openapi_service_mock): securitySchemes_mock = MagicMock() securitySchemes_mock.raw_element = {"apiKey": {"in": "header", "name": "x-api-key", "type": "apiKey"}} openapi_service_mock.components.securitySchemes = securitySchemes_mock connector._authenticate_service(openapi_service_mock, {"apiKey": "some_fake_token"}) def test_authenticate_service_having_authentication_dict_but_unsupported_auth( self, connector, openapi_service_mock ): securitySchemes_mock = MagicMock() securitySchemes_mock.raw_element = {"oauth2": {"type": "oauth2"}} openapi_service_mock.components.securitySchemes = securitySchemes_mock with pytest.raises(ValueError): connector._authenticate_service(openapi_service_mock, {"apiKey": "some_fake_token"}) def test_invoke_method_valid(self, connector, openapi_service_mock): # Test valid method invocation method_invocation_descriptor = {"name": "test_method", "arguments": {}} openapi_service_mock.call_test_method = Mock(return_value="response") result = connector._invoke_method(openapi_service_mock, method_invocation_descriptor) assert result == "response" def test_invoke_method_invalid(self, connector, openapi_service_mock): # Test invalid method invocation method_invocation_descriptor = {"name": "invalid_method", "arguments": {}} with pytest.raises(RuntimeError): connector._invoke_method(openapi_service_mock, method_invocation_descriptor) def test_for_internal_raw_data_field(self): # see https://github.com/deepset-ai/haystack/pull/6772 for details model = Model(data={}, schema={}) assert hasattr(model, "_raw_data"), ( "openapi3 changed. Model should have a _raw_data field, we rely on it in OpenAPIServiceConnector" " to get the raw data from the service response" ) @patch("haystack.components.connectors.openapi_service.OpenAPI") def test_run(self, openapi_mock, test_files_path): connector = OpenAPIServiceConnector() spec_path = test_files_path / "json" / "github_compare_branch_openapi_spec.json" spec = json.loads((spec_path).read_text()) mock_message = json.dumps( [ { "id": "call_NJr1NBz2Th7iUWJpRIJZoJIA", "function": { "arguments": '{"parameters": {"basehead": "main...some_branch", "owner": "deepset-ai", "repo": "haystack"}}', "name": "compare_branches", }, "type": "function", } ] ) messages = [ChatMessage.from_assistant(mock_message)] mock_service = Mock( call_compare_branches=Mock(return_value=Mock(_raw_data="some_data")), components=Mock(securitySchemes=Mock(raw_element={"apikey": {"type": "apiKey"}})), ) openapi_mock.return_value = mock_service result = connector.run(messages=messages, service_openapi_spec=spec, service_credentials="fake_key") openapi_mock.assert_called_once_with(spec) mock_service.authenticate.assert_called_once_with("apikey", "fake_key") mock_service.call_compare_branches.assert_called_once_with( parameters={"basehead": "main...some_branch", "owner": "deepset-ai", "repo": "haystack"} ) assert result == {"service_response": [ChatMessage.from_user('"some_data"')]}