diff --git a/graphene_linked_events/rest_client.py b/graphene_linked_events/rest_client.py index 0b2fba2b..6a0a760f 100644 --- a/graphene_linked_events/rest_client.py +++ b/graphene_linked_events/rest_client.py @@ -2,12 +2,11 @@ class LinkedEventsApiClient(object): - CONNECTION_TIMEOUT = 20 - def __init__(self, config) -> None: self.root = config["ROOT"] self.api_key = config["API_KEY"] self.data_source = config["DATA_SOURCE"] + self.timeout = config.get("TIMEOUT", 60) super().__init__() def get_actions(self, resource=None): @@ -34,13 +33,13 @@ def retrieve(self, resource, id, params=None, is_event_staff=False): params=formatted_params, headers=headers, cookies=cookies, - timeout=self.CONNECTION_TIMEOUT, + timeout=self.timeout, ) return requests.request( actions["retrieve"]["method"], actions["retrieve"]["url"].format(id), params=formatted_params, - timeout=self.CONNECTION_TIMEOUT, + timeout=self.timeout, ) def list(self, resource, filter_list=None, is_event_staff=False): @@ -61,13 +60,13 @@ def list(self, resource, filter_list=None, is_event_staff=False): params=filter_params, headers=headers, cookies=cookies, - timeout=self.CONNECTION_TIMEOUT, + timeout=self.timeout, ) return requests.request( actions["list"]["method"], actions["list"]["url"], params=filter_params, - timeout=self.CONNECTION_TIMEOUT, + timeout=self.timeout, ) def create(self, resource, body): @@ -78,7 +77,7 @@ def create(self, resource, body): actions["create"]["url"], data=body, headers=headers, - timeout=self.CONNECTION_TIMEOUT, + timeout=self.timeout, ) def update(self, resource, id, body): @@ -89,7 +88,7 @@ def update(self, resource, id, body): actions["update"]["url"].format(id), data=body, headers=headers, - timeout=self.CONNECTION_TIMEOUT, + timeout=self.timeout, ) def delete(self, resource, id): @@ -99,7 +98,7 @@ def delete(self, resource, id): actions["delete"]["method"], actions["delete"]["url"].format(id), headers=headers, - timeout=self.CONNECTION_TIMEOUT, + timeout=self.timeout, ) # Special action to full-text search generic resources @@ -110,7 +109,7 @@ def search(self, params): action["method"], action["url"], params=search_params, - timeout=self.CONNECTION_TIMEOUT, + timeout=self.timeout, ) return response @@ -123,7 +122,7 @@ def upload(self, resource, body, files): data=body, files=files, headers=headers, - timeout=self.CONNECTION_TIMEOUT, + timeout=self.timeout, ) @staticmethod diff --git a/graphene_linked_events/tests/test_rest_client.py b/graphene_linked_events/tests/test_rest_client.py new file mode 100644 index 00000000..5eaf24bf --- /dev/null +++ b/graphene_linked_events/tests/test_rest_client.py @@ -0,0 +1,161 @@ +from unittest.mock import Mock, patch + +from graphene_linked_events.rest_client import LinkedEventsApiClient + + +class TestLinkedEventsApiClientTimeout: + """Tests for LinkedEventsApiClient timeout configuration.""" + + def get_base_config(self, timeout=None): + """Return a base config dict, optionally with timeout.""" + config = { + "ROOT": "https://api.example.com/v1/", + "API_KEY": "test-api-key", + "DATA_SOURCE": "test-source", + } + if timeout is not None: + config["TIMEOUT"] = timeout + return config + + def test_timeout_from_config(self): + """Client should use timeout value from config.""" + config = self.get_base_config(timeout=90) + client = LinkedEventsApiClient(config) + assert client.timeout == 90 + + def test_timeout_default_when_not_in_config(self): + """Client should use default timeout (60s) when not in config.""" + config = self.get_base_config() + client = LinkedEventsApiClient(config) + assert client.timeout == 60 + + def test_timeout_zero_is_allowed(self): + """Client should accept zero timeout from config.""" + config = self.get_base_config(timeout=0) + client = LinkedEventsApiClient(config) + assert client.timeout == 0 + + @patch("graphene_linked_events.rest_client.requests.request") + def test_retrieve_uses_configured_timeout(self, mock_request): + """Retrieve method should pass configured timeout to requests.""" + mock_request.return_value = Mock(status_code=200) + config = self.get_base_config(timeout=45) + client = LinkedEventsApiClient(config) + + client.retrieve("event", "123") + + mock_request.assert_called_once() + assert mock_request.call_args.kwargs["timeout"] == 45 + + @patch("graphene_linked_events.rest_client.requests.request") + def test_list_uses_configured_timeout(self, mock_request): + """List method should pass configured timeout to requests.""" + mock_request.return_value = Mock(status_code=200) + config = self.get_base_config(timeout=45) + client = LinkedEventsApiClient(config) + + client.list("event") + + mock_request.assert_called_once() + assert mock_request.call_args.kwargs["timeout"] == 45 + + @patch("graphene_linked_events.rest_client.requests.request") + def test_create_uses_configured_timeout(self, mock_request): + """Create method should pass configured timeout to requests.""" + mock_request.return_value = Mock(status_code=201) + config = self.get_base_config(timeout=45) + client = LinkedEventsApiClient(config) + + client.create("event", '{"name": "Test"}') + + mock_request.assert_called_once() + assert mock_request.call_args.kwargs["timeout"] == 45 + + @patch("graphene_linked_events.rest_client.requests.request") + def test_update_uses_configured_timeout(self, mock_request): + """Update method should pass configured timeout to requests.""" + mock_request.return_value = Mock(status_code=200) + config = self.get_base_config(timeout=45) + client = LinkedEventsApiClient(config) + + client.update("event", "123", '{"name": "Updated"}') + + mock_request.assert_called_once() + assert mock_request.call_args.kwargs["timeout"] == 45 + + @patch("graphene_linked_events.rest_client.requests.request") + def test_delete_uses_configured_timeout(self, mock_request): + """Delete method should pass configured timeout to requests.""" + mock_request.return_value = Mock(status_code=204) + config = self.get_base_config(timeout=45) + client = LinkedEventsApiClient(config) + + client.delete("event", "123") + + mock_request.assert_called_once() + assert mock_request.call_args.kwargs["timeout"] == 45 + + @patch("graphene_linked_events.rest_client.requests.request") + def test_search_uses_configured_timeout(self, mock_request): + """Search method should pass configured timeout to requests.""" + mock_request.return_value = Mock(status_code=200) + config = self.get_base_config(timeout=45) + client = LinkedEventsApiClient(config) + + # Mock get_actions to return a valid search action + # (search is a special action that doesn't use resource param) + with patch.object( + client, + "get_actions", + return_value={"search": {"method": "GET", "url": client.root + "search/"}}, + ): + client.search({"q": "test"}) + + mock_request.assert_called_once() + assert mock_request.call_args.kwargs["timeout"] == 45 + + @patch("graphene_linked_events.rest_client.requests.request") + def test_upload_uses_configured_timeout(self, mock_request): + """Upload method should pass configured timeout to requests.""" + mock_request.return_value = Mock(status_code=201) + config = self.get_base_config(timeout=45) + client = LinkedEventsApiClient(config) + + client.upload("image", {"name": "test"}, {"file": b"data"}) + + mock_request.assert_called_once() + assert mock_request.call_args.kwargs["timeout"] == 45 + + +class TestLinkedEventsApiClientConfig: + """Tests for LinkedEventsApiClient configuration.""" + + def test_client_stores_root_url(self): + """Client should store root URL from config.""" + config = { + "ROOT": "https://api.example.com/v1/", + "API_KEY": "test-key", + "DATA_SOURCE": "test-source", + } + client = LinkedEventsApiClient(config) + assert client.root == "https://api.example.com/v1/" + + def test_client_stores_api_key(self): + """Client should store API key from config.""" + config = { + "ROOT": "https://api.example.com/v1/", + "API_KEY": "my-secret-key", + "DATA_SOURCE": "test-source", + } + client = LinkedEventsApiClient(config) + assert client.api_key == "my-secret-key" + + def test_client_stores_data_source(self): + """Client should store data source from config.""" + config = { + "ROOT": "https://api.example.com/v1/", + "API_KEY": "test-key", + "DATA_SOURCE": "palvelutarjotin", + } + client = LinkedEventsApiClient(config) + assert client.data_source == "palvelutarjotin" diff --git a/palvelutarjotin/settings.py b/palvelutarjotin/settings.py index c3bc5149..76dcfcc5 100644 --- a/palvelutarjotin/settings.py +++ b/palvelutarjotin/settings.py @@ -68,6 +68,7 @@ ENABLE_GRAPHIQL=(bool, False), LINKED_EVENTS_API_ROOT=(str, "https://api.hel.fi/linkedevents/v1/"), LINKED_EVENTS_API_KEY=(str, ""), + LINKED_EVENTS_API_TIMEOUT=(int, 60), LINKED_EVENTS_DATA_SOURCE=(str, "palvelutarjotin"), SERVICEMAP_API_ROOT=(str, "https://www.hel.fi/palvelukarttaws/rest/v4/unit/"), NOTIFICATION_SERVICE_SMS_ENABLED=(bool, True), @@ -432,6 +433,7 @@ def sentry_traces_sampler(sampling_context: SamplingContext) -> float: "ROOT": env.str("LINKED_EVENTS_API_ROOT"), "API_KEY": env.str("LINKED_EVENTS_API_KEY"), "DATA_SOURCE": env.str("LINKED_EVENTS_DATA_SOURCE"), + "TIMEOUT": env.int("LINKED_EVENTS_API_TIMEOUT"), } SERVICEMAP_API_CONFIG = {"ROOT": env.str("SERVICEMAP_API_ROOT")}