/ test / components / connectors / test_openapi_connector.py
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