"""Unit tests for the SplitIOClient module.""" from unittest.mock import MagicMock, patch from config import SPLITIO_BASE_URL import pytest from src.connectors.splitio_client import SplitIOClient from src.utils.custom_exceptions import ( HTTPRequestError, InvalidContentTypeError, InvalidResponseError, JSONParsingError, RateLimitExceededError ) def get_mock_response(status_code=200, json_data=None, content_type='application/json', headers=None, text='OK'): """Create a mock response object.""" mock_resp = MagicMock() mock_resp.status_code = status_code mock_resp.headers = headers or {'Content-Type': content_type} mock_resp.text = text mock_resp.json.return_value = json_data return mock_resp @pytest.fixture def client(): """Fixture to create a SplitIOClient instance for testing.""" return SplitIOClient(api_key='test_api_key', workspace_id='test_workspace_id') def test_init_without_api_key_raises_value_error(): """Test that SplitIOClient raises ValueError if api_key is not set.""" with pytest.raises(ValueError, match='api_key must be set. It cannot be empty or None.'): SplitIOClient(api_key='', workspace_id='test_workspace_id') def test_init_without_workspace_id_raises_value_error(): """Test that SplitIOClient raises ValueError if workspace_id is not set.""" with pytest.raises(ValueError, match='workspace_id must be set. It cannot be empty or None.'): SplitIOClient(api_key='test_api_key', workspace_id='') def test_init_with_valid_workspace_id(client): """Test that SplitIOClient initializes with a valid workspace_id.""" assert client.workspace_id == 'test_workspace_id' assert 'Authorization' in client.session.headers @patch('src.connectors.splitio_client.requests.Session.request') def test_request_with_custom_retry_successful_request(mock_request, client): """Test that _request_with_custom_retry returns JSON data on success.""" mock_request.return_value = get_mock_response(json_data={'key': 'value'}) result = client._request_with_custom_retry('GET', 'http://example.com') assert result == {'key': 'value'} @patch('src.connectors.splitio_client.requests.Session.request') def test_request_with_custom_retry_on_429_then_success(mock_request, client): """Test that _request_with_custom_retry retries on 429 and succeeds.""" resp_429 = get_mock_response(429, headers={ 'Content-Type': 'application/json', 'X-RateLimit-Reset-Seconds-Org': '1', 'X-RateLimit-Reset-Seconds-IP': '2' }) resp_200 = get_mock_response(json_data={'ok': True}) mock_request.side_effect = [resp_429, resp_200] with patch('time.sleep') as sleep_mock: result = client._request_with_custom_retry('GET', 'http://example.com') assert result == {'ok': True} assert sleep_mock.called @patch('src.connectors.splitio_client.requests.Session.request') def test_request_with_custom_retry_exceeds_then_raises(mock_request, client): """Test that _request_with_custom_retry raises RateLimitExceededError after retries.""" resp_429 = get_mock_response(429, headers={ 'Content-Type': 'application/json', 'X-RateLimit-Reset-Seconds-Org': '0', 'X-RateLimit-Reset-Seconds-IP': '0' }) mock_request.side_effect = [resp_429] * 6 with patch('time.sleep'), pytest.raises(RateLimitExceededError): client._request_with_custom_retry('GET', 'http://example.com', retries=5) @patch('src.connectors.splitio_client.requests.Session.request') def test_request_with_custom_retry_raises_http_error(mock_request, client): """Test that _request_with_custom_retry raises HTTPRequestError for HTTP errors.""" mock_request.return_value = get_mock_response(404, text='Not Found') with pytest.raises(HTTPRequestError, match='HTTP 404: Not Found'): client._request_with_custom_retry('GET', 'http://example.com') @patch('src.connectors.splitio_client.requests.Session.request') def test_request_with_custom_retry_raises_invalid_content_type_error(mock_request, client): """Test that _request_with_custom_retry raises InvalidContentTypeError for unexpected content type.""" mock_request.return_value = get_mock_response(content_type='text/html') with pytest.raises(InvalidContentTypeError, match='Unexpected content type: text/html'): client._request_with_custom_retry('GET', 'http://example.com') @patch('src.connectors.splitio_client.requests.Session.request') def test_request_with_custom_retry_raises_json_parsing_error(mock_request, client): """Test that _request_with_custom_retry raises JSONParsingError for invalid JSON.""" mock_resp = get_mock_response() mock_resp.json.side_effect = ValueError('No JSON could be decoded') mock_request.return_value = mock_resp with pytest.raises(JSONParsingError, match='Failed to parse JSON'): client._request_with_custom_retry('GET', 'http://example.com') @patch('src.connectors.splitio_client.requests.Session.request') def test_request_with_custom_retry_raises_invalid_response_error(mock_request, client): """Test that _request_with_custom_retry raises InvalidResponseError for unexpected JSON type.""" mock_request.return_value = get_mock_response(json_data='not_a_dict_or_list') with pytest.raises(InvalidResponseError, match='Unexpected JSON format'): client._request_with_custom_retry('GET', 'http://example.com') @patch('src.connectors.splitio_client.SplitIOClient._request_with_custom_retry') def test_get_environments_success(mock_request_with_retry, client): """Test that get_environments returns a list of environments.""" mock_request_with_retry.return_value = [{'id': 'env_1', 'name': 'prod'}, {'id': 'env_2', 'name': 'qa'}] result = client.get_environments() assert result == [{'id': 'env_1', 'name': 'prod'}, {'id': 'env_2', 'name': 'qa'}] mock_request_with_retry.assert_called_once() mock_request_with_retry.assert_called_with( 'GET', f'{SPLITIO_BASE_URL}/environments/ws/{client.workspace_id}' ) @patch('src.connectors.splitio_client.SplitIOClient._request_with_custom_retry') def test_get_environments_invalid_response(mock_request_with_retry, client): """Test that get_environments raises InvalidResponseError on unexpected response.""" mock_response = { 'data': [{'id': 'env_1', 'name': 'prod'}, {'id': 'env_2', 'name': 'qa'}] } mock_request_with_retry.return_value = mock_response with pytest.raises(InvalidResponseError, match='Expected a list of environments.'): client.get_environments() @patch('src.connectors.splitio_client.SplitIOClient._request_with_custom_retry') def test_get_feature_flag_metadata_success(mock_request_with_retry, client): """Test that get_feature_flag_metadata returns a dict containing feature flag metadata.""" mock_request_with_retry.return_value = {'name': 'flag_1', 'type': 'boolean'} result = client.get_feature_flag_metadata('flag_1') assert result == {'name': 'flag_1', 'type': 'boolean'} mock_request_with_retry.assert_called_once() mock_request_with_retry.assert_called_with( 'GET', f'{SPLITIO_BASE_URL}/splits/ws/{client.workspace_id}/flag_1' ) @patch('src.connectors.splitio_client.SplitIOClient._request_with_custom_retry') def test_get_feature_flag_metadata_http_404_returns_none(mock_request_with_retry, client): """Test that get_feature_flag_metadata returns None when HTTP 404 error is raised.""" mock_request_with_retry.side_effect = HTTPRequestError('Request failed with HTTP 404 Not Found') result = client.get_feature_flag_metadata('flag_1') assert result is None mock_request_with_retry.assert_called_once_with( 'GET', f'{SPLITIO_BASE_URL}/splits/ws/{client.workspace_id}/flag_1' ) @patch('src.connectors.splitio_client.SplitIOClient._request_with_custom_retry') def test_get_feature_flag_metadata_other_http_error_raises(mock_request_with_retry, client): """Test that get_feature_flag_metadata re-raises HTTPRequestError for non-404 errors.""" mock_request_with_retry.side_effect = HTTPRequestError('Request failed with HTTP 500 Internal Server Error') with pytest.raises(HTTPRequestError, match='HTTP 500'): client.get_feature_flag_metadata('flag_1') @patch('src.connectors.splitio_client.SplitIOClient._request_with_custom_retry') def test_get_feature_flag_metadata_invalid_response(mock_request_with_retry, client): """Test that get_feature_flag_metadata raises InvalidResponseError on unexpected response.""" mock_response = [{'name': 'flag_1', 'type': 'boolean'}] mock_request_with_retry.return_value = mock_response with pytest.raises(InvalidResponseError, match='Expected a dict response for feature flag metadata.'): client.get_feature_flag_metadata('flag_1') @patch('src.connectors.splitio_client.SplitIOClient._request_with_custom_retry') def test_get_feature_flag_definition_success(mock_request_with_retry, client): """Test that get_feature_flag_definition returns a dict containing feature flag definition.""" mock_request_with_retry.return_value = {'name': 'flag_1', 'type': 'boolean'} result = client.get_feature_flag_definition(env_id='env_1', flag_name='flag_1') assert result == {'name': 'flag_1', 'type': 'boolean'} mock_request_with_retry.assert_called_once() mock_request_with_retry.assert_called_with( 'GET', f'{SPLITIO_BASE_URL}/splits/ws/{client.workspace_id}/flag_1/environments/env_1' ) @patch('src.connectors.splitio_client.SplitIOClient._request_with_custom_retry') def test_get_feature_flag_definition_http_404_returns_none(mock_request_with_retry, client): """Test that get_feature_flag_definition returns None when HTTP 404 error is raised.""" mock_request_with_retry.side_effect = HTTPRequestError('Request failed with HTTP 404 Not Found') result = client.get_feature_flag_definition(env_id='env_1', flag_name='nonexistent_flag') assert result is None mock_request_with_retry.assert_called_once_with( 'GET', f'{SPLITIO_BASE_URL}/splits/ws/{client.workspace_id}/nonexistent_flag/environments/env_1' ) @patch('src.connectors.splitio_client.SplitIOClient._request_with_custom_retry') def test_get_feature_flag_definition_other_http_error_raises(mock_request_with_retry, client): """Test that get_feature_flag_definition re-raises HTTPRequestError for non-404 errors.""" mock_request_with_retry.side_effect = HTTPRequestError('Request failed with HTTP 500 Internal Server Error') with pytest.raises(HTTPRequestError, match='HTTP 500'): client.get_feature_flag_definition(env_id='env_1', flag_name='flag_1') @patch('src.connectors.splitio_client.SplitIOClient._request_with_custom_retry') def test_get_feature_flag_definition_invalid_response(mock_request_with_retry, client): """Test that get_feature_flag_definition raises InvalidResponseError on unexpected response.""" mock_response = [{'name': 'flag_1', 'type': 'boolean'}] mock_request_with_retry.return_value = mock_response with pytest.raises(InvalidResponseError, match='Expected a dict response for feature flag definition.'): client.get_feature_flag_definition(env_id='env_1', flag_name='flag_1') @patch('src.connectors.splitio_client.SplitIOClient._request_with_custom_retry') def test_get_all_FF_names_multiple_pages(mock_request, client): """Test that get_all_FF_names returns names across multiple pages.""" mock_request.side_effect = [ {'objects': [{'name': f'flag_{i}'} for i in range(50)]}, {'objects': [{'name': f'flag_{i}'} for i in range(50, 75)]}, ] result = client.get_all_FF_names() expected = [f'flag_{i}' for i in range(75)] assert result == expected assert mock_request.call_count == 2 mock_request.assert_any_call('GET', f'{SPLITIO_BASE_URL}/splits/ws/{client.workspace_id}?offset=0&limit=50') mock_request.assert_any_call('GET', f'{SPLITIO_BASE_URL}/splits/ws/{client.workspace_id}?offset=50&limit=50') @patch('src.connectors.splitio_client.SplitIOClient._request_with_custom_retry') def test_get_all_FF_names_single_page(mock_request, client): """Test that get_all_FF_names returns a single page result correctly.""" mock_request.return_value = { 'objects': [{'name': 'flag_1'}, {'name': 'flag_2'}] } result = client.get_all_FF_names() assert result == ['flag_1', 'flag_2'] mock_request.assert_called_once_with( 'GET', f'{SPLITIO_BASE_URL}/splits/ws/{client.workspace_id}?offset=0&limit=50' ) @patch('src.connectors.splitio_client.SplitIOClient._request_with_custom_retry') def test_get_all_FF_names_invalid_response_type(mock_request, client): """Test that InvalidResponseError is raised when response is not a dict.""" mock_request.return_value = ['not', 'a', 'dict'] with pytest.raises(InvalidResponseError, match='Expected a dict of feature flags.'): client.get_all_FF_names() @patch('src.connectors.splitio_client.SplitIOClient._request_with_custom_retry') def test_get_all_FF_names_ignores_flags_without_name(mock_request, client): """Test that flags without a 'name' key are ignored.""" mock_request.return_value = { 'objects': [{'name': 'flag_1'}, {'id': 'no_name'}, {'name': 'flag_2'}] } result = client.get_all_FF_names() assert result == ['flag_1', 'flag_2']