diff --git a/setup.py b/setup.py index 950aea67..95e041ec 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ TESTS_REQUIRES = [ 'flake8', 'pytest==7.0.1', - 'pytest-mock>=3.5.1', + 'pytest-mock==3.13.0', 'coverage==6.2', 'pytest-cov', 'importlib-metadata==4.2', diff --git a/splitio/api/client.py b/splitio/api/client.py index c58d14e9..073970fc 100644 --- a/splitio/api/client.py +++ b/splitio/api/client.py @@ -28,10 +28,12 @@ class HttpClient(object): AUTH_URL = 'https://auth.split.io/api' TELEMETRY_URL = 'https://telemetry.split.io/api' - def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None): + def __init__(self, request_decorator, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None): """ Class constructor. + :param request_decorator: RequestDecorator instance + :type request_decorator: splitio.api.request_decorator.RequestDecorator :param timeout: How many milliseconds to wait until the server responds. :type timeout: int :param sdk_url: Optional alternative sdk URL. @@ -50,6 +52,7 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, t 'auth': auth_url if auth_url is not None else self.AUTH_URL, 'telemetry': telemetry_url if telemetry_url is not None else self.TELEMETRY_URL, } + self._request_decorator = request_decorator def _build_url(self, server, path): """ @@ -101,7 +104,9 @@ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: headers.update(extra_headers) try: - response = requests.get( + session = requests.Session() + session = self._request_decorator.decorate_headers(session) + response = session.get( self._build_url(server, path), params=query, headers=headers, @@ -110,6 +115,8 @@ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: return HttpResponse(response.status_code, response.text) except Exception as exc: # pylint: disable=broad-except raise HttpClientException('requests library is throwing exceptions') from exc + finally: + session.close() def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # pylint: disable=too-many-arguments """ @@ -137,7 +144,9 @@ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # headers.update(extra_headers) try: - response = requests.post( + session = requests.Session() + session = self._request_decorator.decorate_headers(session) + response = session.post( self._build_url(server, path), json=body, params=query, @@ -147,3 +156,5 @@ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # return HttpResponse(response.status_code, response.text) except Exception as exc: # pylint: disable=broad-except raise HttpClientException('requests library is throwing exceptions') from exc + finally: + session.close() diff --git a/tests/api/test_httpclient.py b/tests/api/test_httpclient.py index 694c9a22..74725cf3 100644 --- a/tests/api/test_httpclient.py +++ b/tests/api/test_httpclient.py @@ -1,6 +1,8 @@ """HTTPClient test module.""" +import pytest from splitio.api import client +from splitio.api.request_decorator import RequestDecorator, NoOpHeaderDecorator, UserCustomHeaderDecorator class HttpClientTests(object): """Http Client test cases.""" @@ -12,8 +14,9 @@ def test_get(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.get', new=get_mock) - httpclient = client.HttpClient() + mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) + + httpclient = client.HttpClient(RequestDecorator(NoOpHeaderDecorator())) response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) call = mocker.call( client.HttpClient.SDK_URL + '/test1', @@ -44,8 +47,8 @@ def test_get_custom_urls(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.get', new=get_mock) - httpclient = client.HttpClient(sdk_url='https://sdk.com', events_url='https://events.com') + mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) + httpclient = client.HttpClient(RequestDecorator(NoOpHeaderDecorator()), sdk_url='https://sdk.com', events_url='https://events.com') response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) call = mocker.call( 'https://sdk.com/test1', @@ -69,6 +72,40 @@ def test_get_custom_urls(self, mocker): assert response.body == 'ok' assert get_mock.mock_calls == [call] + def test_get_custom_headers(self, mocker): + """Test HTTP GET verb requests.""" + response_mock = mocker.Mock() + response_mock.status_code = 200 + response_mock.text = 'ok' + get_mock = mocker.Mock() + get_mock.return_value = response_mock + mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) + + class MyCustomDecorator(UserCustomHeaderDecorator): + def get_header_overrides(self): + return {"UserCustomHeader": "value", "AnotherCustomHeader": "val"} + + global current_session + current_session = None + class RequestDecoratorWrapper(RequestDecorator): + def decorate_headers(self, session): + global current_session + current_session = session + return RequestDecorator.decorate_headers(self, session) + + httpclient = client.HttpClient(RequestDecoratorWrapper(MyCustomDecorator())) + response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) + call = mocker.call( + client.HttpClient.SDK_URL + '/test1', + headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, + params={'param1': 123}, + timeout=None + ) + assert current_session.headers["UserCustomHeader"] == "value" + assert current_session.headers["AnotherCustomHeader"] == "val" + assert response.status_code == 200 + assert response.body == 'ok' + assert get_mock.mock_calls == [call] def test_post(self, mocker): """Test HTTP GET verb requests.""" @@ -77,8 +114,8 @@ def test_post(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.post', new=get_mock) - httpclient = client.HttpClient() + mocker.patch('splitio.api.client.requests.Session.post', new=get_mock) + httpclient = client.HttpClient(RequestDecorator(NoOpHeaderDecorator())) response = httpclient.post('sdk', '/test1', 'some_api_key', {'p1': 'a'}, {'param1': 123}, {'h1': 'abc'}) call = mocker.call( client.HttpClient.SDK_URL + '/test1', @@ -111,8 +148,8 @@ def test_post_custom_urls(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.post', new=get_mock) - httpclient = client.HttpClient(sdk_url='https://sdk.com', events_url='https://events.com') + mocker.patch('splitio.api.client.requests.Session.post', new=get_mock) + httpclient = client.HttpClient(RequestDecorator(NoOpHeaderDecorator()), sdk_url='https://sdk.com', events_url='https://events.com') response = httpclient.post('sdk', '/test1', 'some_api_key', {'p1': 'a'}, {'param1': 123}, {'h1': 'abc'}) call = mocker.call( 'https://sdk.com' + '/test1', @@ -137,3 +174,38 @@ def test_post_custom_urls(self, mocker): assert response.status_code == 200 assert response.body == 'ok' assert get_mock.mock_calls == [call] + + def test_post_custom_headers(self, mocker): + """Test HTTP GET verb requests.""" + response_mock = mocker.Mock() + response_mock.status_code = 200 + response_mock.text = 'ok' + get_mock = mocker.Mock() + get_mock.return_value = response_mock + mocker.patch('splitio.api.client.requests.Session.post', new=get_mock) + class MyCustomDecorator(UserCustomHeaderDecorator): + def get_header_overrides(self): + return {"UserCustomHeader": "value", "AnotherCustomHeader": "val"} + + global current_session + current_session = None + class RequestDecoratorWrapper(RequestDecorator): + def decorate_headers(self, session): + global current_session + current_session = session + return RequestDecorator.decorate_headers(self, session) + + httpclient = client.HttpClient(RequestDecoratorWrapper(MyCustomDecorator())) + response = httpclient.post('sdk', '/test1', 'some_api_key', {'p1': 'a'}, {'param1': 123}, {'h1': 'abc'}) + call = mocker.call( + client.HttpClient.SDK_URL + '/test1', + json={'p1': 'a'}, + headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, + params={'param1': 123}, + timeout=None + ) + assert current_session.headers["UserCustomHeader"] == "value" + assert current_session.headers["AnotherCustomHeader"] == "val" + assert response.status_code == 200 + assert response.body == 'ok' + assert get_mock.mock_calls == [call] \ No newline at end of file