haystack/test/components/connectors/test_openapi_service.py

145 lines
6.7 KiB
Python
Raw Normal View History

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"')]}