test_openapi_connector.py
1 # SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai> 2 # 3 # SPDX-License-Identifier: Apache-2.0 4 5 import json 6 import os 7 from unittest.mock import Mock, patch 8 9 import pytest 10 11 from haystack import Pipeline 12 from haystack.components.connectors.openapi import OpenAPIConnector 13 from haystack.utils import Secret 14 15 # Mock OpenAPI spec for testing 16 MOCK_OPENAPI_SPEC = """ 17 openapi: 3.0.0 18 info: 19 title: Test API 20 version: 1.0.0 21 paths: 22 /search: 23 get: 24 operationId: search 25 parameters: 26 - name: q 27 in: query 28 required: true 29 schema: 30 type: string 31 """ 32 33 34 @pytest.fixture 35 def mock_client(): 36 with patch("haystack.components.connectors.openapi.OpenAPIClient") as mock: 37 client_instance = Mock() 38 mock.from_spec.return_value = client_instance 39 yield client_instance 40 41 42 class TestOpenAPIConnector: 43 def test_init(self, mock_client): 44 # Test initialization with credentials and service_kwargs 45 service_kwargs = {"allowed_operations": ["search"]} 46 connector = OpenAPIConnector( 47 openapi_spec=MOCK_OPENAPI_SPEC, credentials=Secret.from_token("test-token"), service_kwargs=service_kwargs 48 ) 49 assert connector.openapi_spec == MOCK_OPENAPI_SPEC 50 assert connector.credentials.resolve_value() == "test-token" 51 assert connector.service_kwargs == service_kwargs 52 53 # Test initialization without credentials and service_kwargs 54 connector = OpenAPIConnector(openapi_spec=MOCK_OPENAPI_SPEC) 55 assert connector.credentials is None 56 assert connector.service_kwargs == {} 57 58 def test_to_dict(self, monkeypatch): 59 monkeypatch.setenv("ENV_VAR", "test-api-key") 60 service_kwargs = {"allowed_operations": ["search"]} 61 connector = OpenAPIConnector( 62 openapi_spec=MOCK_OPENAPI_SPEC, credentials=Secret.from_env_var("ENV_VAR"), service_kwargs=service_kwargs 63 ) 64 serialized = connector.to_dict() 65 assert serialized == { 66 "type": "haystack.components.connectors.openapi.OpenAPIConnector", 67 "init_parameters": { 68 "openapi_spec": MOCK_OPENAPI_SPEC, 69 "credentials": {"env_vars": ["ENV_VAR"], "type": "env_var", "strict": True}, 70 "service_kwargs": service_kwargs, 71 }, 72 } 73 74 def test_from_dict(self, monkeypatch): 75 monkeypatch.setenv("ENV_VAR", "test-api-key") 76 service_kwargs = {"allowed_operations": ["search"]} 77 data = { 78 "type": "haystack.components.connectors.openapi.OpenAPIConnector", 79 "init_parameters": { 80 "openapi_spec": MOCK_OPENAPI_SPEC, 81 "credentials": {"env_vars": ["ENV_VAR"], "type": "env_var", "strict": True}, 82 "service_kwargs": service_kwargs, 83 }, 84 } 85 connector = OpenAPIConnector.from_dict(data) 86 assert connector.openapi_spec == MOCK_OPENAPI_SPEC 87 assert connector.credentials == Secret.from_env_var("ENV_VAR") 88 assert connector.service_kwargs == service_kwargs 89 90 def test_run(self, mock_client): 91 service_kwargs = {"allowed_operations": ["search"]} 92 connector = OpenAPIConnector( 93 openapi_spec=MOCK_OPENAPI_SPEC, credentials=Secret.from_token("test-token"), service_kwargs=service_kwargs 94 ) 95 96 # Mock the response from the client 97 mock_client.invoke.return_value = {"results": ["test result"]} 98 99 # Test with arguments 100 response = connector.run(operation_id="search", arguments={"q": "test query"}) 101 mock_client.invoke.assert_called_with({"name": "search", "arguments": {"q": "test query"}}) 102 assert response == {"response": {"results": ["test result"]}} 103 104 # Test without arguments 105 response = connector.run(operation_id="search") 106 mock_client.invoke.assert_called_with({"name": "search", "arguments": {}}) 107 108 def test_in_pipeline(self, mock_client): 109 mock_client.invoke.return_value = {"results": ["test result"]} 110 111 connector = OpenAPIConnector(openapi_spec=MOCK_OPENAPI_SPEC, credentials=Secret.from_token("test-token")) 112 113 pipe = Pipeline() 114 pipe.add_component("api", connector) 115 116 # Test pipeline execution 117 results = pipe.run(data={"api": {"operation_id": "search", "arguments": {"q": "test query"}}}) 118 119 assert results == {"api": {"response": {"results": ["test result"]}}} 120 121 def test_from_dict_fail_wo_env_var(self, monkeypatch): 122 monkeypatch.delenv("ENV_VAR", raising=False) 123 data = { 124 "type": "haystack.components.connectors.openapi.OpenAPIConnector", 125 "init_parameters": { 126 "openapi_spec": MOCK_OPENAPI_SPEC, 127 "credentials": {"env_vars": ["ENV_VAR"], "type": "env_var", "strict": True}, 128 }, 129 } 130 with pytest.raises(ValueError, match="None of the .* environment variables are set"): 131 OpenAPIConnector.from_dict(data) 132 133 def test_serde_in_pipeline(self, monkeypatch): 134 """ 135 Test serialization/deserialization of OpenAPIConnector in a Pipeline, 136 including detailed dictionary validation 137 """ 138 monkeypatch.setenv("API_KEY", "test-api-key") 139 140 # Create connector with specific configuration 141 connector = OpenAPIConnector( 142 openapi_spec=MOCK_OPENAPI_SPEC, 143 credentials=Secret.from_env_var("API_KEY"), 144 service_kwargs={"allowed_operations": ["search"]}, 145 ) 146 147 # Create and configure pipeline 148 pipeline = Pipeline() 149 pipeline.add_component("api", connector) 150 151 # Get pipeline dictionary and verify its structure 152 pipeline_dict = pipeline.to_dict() 153 assert pipeline_dict == { 154 "metadata": {}, 155 "max_runs_per_component": 100, 156 "connection_type_validation": True, 157 "components": { 158 "api": { 159 "type": "haystack.components.connectors.openapi.OpenAPIConnector", 160 "init_parameters": { 161 "openapi_spec": MOCK_OPENAPI_SPEC, 162 "credentials": {"env_vars": ["API_KEY"], "type": "env_var", "strict": True}, 163 "service_kwargs": {"allowed_operations": ["search"]}, 164 }, 165 } 166 }, 167 "connections": [], 168 } 169 170 # Test YAML serialization/deserialization 171 pipeline_yaml = pipeline.dumps() 172 new_pipeline = Pipeline.loads(pipeline_yaml) 173 assert new_pipeline == pipeline 174 175 # Verify the loaded pipeline's connector has the same configuration 176 loaded_connector = new_pipeline.get_component("api") 177 assert loaded_connector.openapi_spec == connector.openapi_spec 178 assert loaded_connector.credentials == connector.credentials 179 assert loaded_connector.service_kwargs == connector.service_kwargs 180 181 182 @pytest.mark.integration 183 class TestOpenAPIConnectorIntegration: 184 @pytest.mark.skipif( 185 not os.environ.get("SERPERDEV_API_KEY", None), 186 reason="Export an env var called SERPERDEV_API_KEY to run this test.", 187 ) 188 @pytest.mark.integration 189 def test_serper_dev_integration(self): 190 component = OpenAPIConnector( 191 openapi_spec="https://bit.ly/serperdev_openapi", credentials=Secret.from_env_var("SERPERDEV_API_KEY") 192 ) 193 response = component.run(operation_id="search", arguments={"q": "Who was Nikola Tesla?"}) 194 assert isinstance(response, dict) 195 assert "response" in response 196 197 @pytest.mark.integration 198 @pytest.mark.flaky(reruns=3, reruns_delay=5) 199 def test_open_meteo_integration(self): 200 open_meteo_spec = { 201 "openapi": "3.0.0", 202 "info": {"title": "Open-Meteo Historical Weather API", "version": "1.0.0"}, 203 "servers": [{"url": "https://archive-api.open-meteo.com"}], 204 "paths": { 205 "/v1/archive": { 206 "get": { 207 "operationId": "get_archive", 208 "parameters": [ 209 {"name": "latitude", "in": "query", "required": True, "schema": {"type": "number"}}, 210 {"name": "longitude", "in": "query", "required": True, "schema": {"type": "number"}}, 211 {"name": "start_date", "in": "query", "required": True, "schema": {"type": "string"}}, 212 {"name": "end_date", "in": "query", "required": True, "schema": {"type": "string"}}, 213 {"name": "daily", "in": "query", "required": False, "schema": {"type": "string"}}, 214 ], 215 "responses": {"200": {"description": "Historical weather data"}}, 216 } 217 } 218 }, 219 } 220 component = OpenAPIConnector(openapi_spec=json.dumps(open_meteo_spec)) 221 response = component.run( 222 operation_id="get_archive", 223 arguments={ 224 "latitude": 52.52, 225 "longitude": 13.41, 226 "start_date": "2024-01-01", 227 "end_date": "2024-01-07", 228 "daily": "temperature_2m_max", 229 }, 230 ) 231 assert isinstance(response, dict) 232 assert "response" in response 233 234 weather_data = response["response"] 235 assert isinstance(weather_data, dict) 236 assert weather_data["latitude"] == pytest.approx(52.52, abs=0.1) 237 assert weather_data["longitude"] == pytest.approx(13.41, abs=0.1) 238 assert "daily" in weather_data 239 assert "temperature_2m_max" in weather_data["daily"] 240 assert len(weather_data["daily"]["temperature_2m_max"]) == 7