From d422838b4cf1c0f5f4531ff9bad1848ae6a04b16 Mon Sep 17 00:00:00 2001 From: Mobile-Crest Date: Wed, 3 Dec 2025 18:15:22 +0100 Subject: [PATCH] add test for http client and connection utils --- .../unit_test/common/test_connection_utils.py | 340 ++++++++++ test/unit_test/common/test_http_client.py | 620 ++++++++++++++++++ 2 files changed, 960 insertions(+) create mode 100644 test/unit_test/common/test_connection_utils.py create mode 100644 test/unit_test/common/test_http_client.py diff --git a/test/unit_test/common/test_connection_utils.py b/test/unit_test/common/test_connection_utils.py new file mode 100644 index 00000000000..9c656d59017 --- /dev/null +++ b/test/unit_test/common/test_connection_utils.py @@ -0,0 +1,340 @@ +# +# Copyright 2025 The InfiniFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import asyncio +import os +import time +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +import trio +import anyio + +from common.connection_utils import timeout, construct_response, sync_construct_response +from common.constants import RetCode + + +class TestTimeoutDecorator: + """Test cases for the timeout decorator""" + + def test_sync_function_success(self): + """Test timeout decorator with successful sync function""" + @timeout(seconds=5) + def fast_function(): + return "success" + + result = fast_function() + assert result == "success" + + def test_sync_function_with_args(self): + """Test timeout decorator with sync function that has arguments""" + @timeout(seconds=5) + def add_numbers(a, b): + return a + b + + result = add_numbers(3, 4) + assert result == 7 + + def test_sync_function_raises_exception(self): + """Test timeout decorator propagates exceptions from sync function""" + @timeout(seconds=5) + def failing_function(): + raise ValueError("Test error") + + with pytest.raises(ValueError, match="Test error"): + failing_function() + + def test_sync_function_timeout_disabled(self): + """Test sync function when timeout is disabled (no ENABLE_TIMEOUT_ASSERTION)""" + @timeout(seconds=1) + def slow_function(): + time.sleep(0.1) + return "completed" + + # Without ENABLE_TIMEOUT_ASSERTION, timeout should not trigger + result = slow_function() + assert result == "completed" + + def test_sync_function_timeout_enabled(self): + """Test sync function timeout when ENABLE_TIMEOUT_ASSERTION is set""" + @timeout(seconds=0.1, attempts=1) + def slow_function(): + time.sleep(2) + return "should not reach" + + with patch.dict(os.environ, {"ENABLE_TIMEOUT_ASSERTION": "1"}): + with pytest.raises(TimeoutError, match="timed out after 0.1 seconds and 1 attempts"): + slow_function() + + def test_sync_function_with_string_seconds(self): + """Test timeout decorator with string seconds parameter""" + @timeout(seconds="5") + def fast_function(): + return "success" + + result = fast_function() + assert result == "success" + + def test_sync_function_multiple_attempts(self): + """Test sync function with multiple attempts""" + call_count = 0 + + @timeout(seconds=0.1, attempts=3) + def function_with_attempts(): + nonlocal call_count + call_count += 1 + time.sleep(0.05) + return f"attempt_{call_count}" + + result = function_with_attempts() + assert result == "attempt_1" + + @pytest.mark.anyio + async def test_async_function_success(self): + """Test timeout decorator with successful async function""" + @timeout(seconds=5) + async def fast_async_function(): + await anyio.sleep(0.01) + return "async_success" + + result = await fast_async_function() + assert result == "async_success" + + @pytest.mark.anyio + async def test_async_function_with_args(self): + """Test timeout decorator with async function that has arguments""" + @timeout(seconds=5) + async def async_multiply(x, y): + await anyio.sleep(0.01) + return x * y + + result = await async_multiply(6, 7) + assert result == 42 + + @pytest.mark.anyio + async def test_async_function_none_timeout(self): + """Test async function with None timeout (no timeout applied)""" + @timeout(seconds=None) + async def async_function(): + await anyio.sleep(0.01) + return "no_timeout" + + result = await async_function() + assert result == "no_timeout" + + @pytest.mark.anyio + async def test_async_function_timeout_disabled(self): + """Test async function when timeout is disabled""" + @timeout(seconds=1) + async def slow_async_function(): + await anyio.sleep(0.1) + return "completed" + + # Without ENABLE_TIMEOUT_ASSERTION, timeout should not trigger + result = await slow_async_function() + assert result == "completed" + + @pytest.mark.anyio + async def test_async_function_multiple_attempts(self): + """Test async function with multiple attempts""" + call_count = 0 + + @timeout(seconds=0.1, attempts=3) + async def function_with_attempts(): + nonlocal call_count + call_count += 1 + await anyio.sleep(0.05) + return f"attempt_{call_count}" + + result = await function_with_attempts() + assert result == "attempt_1" + + +class TestConstructResponse: + """Test cases for construct_response function""" + + @pytest.mark.anyio + async def test_construct_response_default(self): + """Test construct_response with default parameters""" + with patch('common.connection_utils.make_response', new_callable=AsyncMock) as mock_make_response: + with patch('common.connection_utils.jsonify') as mock_jsonify: + mock_response = MagicMock() + mock_response.headers = {} + mock_make_response.return_value = mock_response + mock_jsonify.return_value = {"code": RetCode.SUCCESS, "message": "success"} + + response = await construct_response() + + mock_jsonify.assert_called_once_with({"code": RetCode.SUCCESS, "message": "success"}) + assert response.headers["Access-Control-Allow-Origin"] == "*" + assert response.headers["Access-Control-Allow-Method"] == "*" + assert response.headers["Access-Control-Allow-Headers"] == "*" + assert response.headers["Access-Control-Expose-Headers"] == "Authorization" + + @pytest.mark.anyio + async def test_construct_response_with_data(self): + """Test construct_response with data parameter""" + with patch('common.connection_utils.make_response', new_callable=AsyncMock) as mock_make_response: + with patch('common.connection_utils.jsonify') as mock_jsonify: + mock_response = MagicMock() + mock_response.headers = {} + mock_make_response.return_value = mock_response + + test_data = {"key": "value"} + response = await construct_response(data=test_data) + + call_args = mock_jsonify.call_args[0][0] + assert call_args["code"] == RetCode.SUCCESS + assert call_args["message"] == "success" + assert call_args["data"] == test_data + + @pytest.mark.anyio + async def test_construct_response_with_error_code(self): + """Test construct_response with error code""" + with patch('common.connection_utils.make_response', new_callable=AsyncMock) as mock_make_response: + with patch('common.connection_utils.jsonify') as mock_jsonify: + mock_response = MagicMock() + mock_response.headers = {} + mock_make_response.return_value = mock_response + + response = await construct_response( + code=RetCode.ARGUMENT_ERROR, + message="Invalid argument" + ) + + call_args = mock_jsonify.call_args[0][0] + assert call_args["code"] == RetCode.ARGUMENT_ERROR + assert call_args["message"] == "Invalid argument" + + @pytest.mark.anyio + async def test_construct_response_with_auth(self): + """Test construct_response with auth token""" + with patch('common.connection_utils.make_response', new_callable=AsyncMock) as mock_make_response: + with patch('common.connection_utils.jsonify') as mock_jsonify: + mock_response = MagicMock() + mock_response.headers = {} + mock_make_response.return_value = mock_response + + auth_token = "Bearer test_token" + response = await construct_response(auth=auth_token) + + assert response.headers["Authorization"] == auth_token + + @pytest.mark.anyio + async def test_construct_response_none_values_excluded(self): + """Test that None values are excluded from response (except code)""" + with patch('common.connection_utils.make_response', new_callable=AsyncMock) as mock_make_response: + with patch('common.connection_utils.jsonify') as mock_jsonify: + mock_response = MagicMock() + mock_response.headers = {} + mock_make_response.return_value = mock_response + + response = await construct_response( + code=RetCode.SUCCESS, + message=None, + data=None + ) + + call_args = mock_jsonify.call_args[0][0] + assert "code" in call_args + assert "message" not in call_args + assert "data" not in call_args + + +class TestSyncConstructResponse: + """Test cases for sync_construct_response function""" + + def test_sync_construct_response_default(self): + """Test sync_construct_response with default parameters""" + with patch('flask.make_response') as mock_make_response: + with patch('flask.jsonify') as mock_jsonify: + mock_response = MagicMock() + mock_response.headers = {} + mock_make_response.return_value = mock_response + mock_jsonify.return_value = {"code": RetCode.SUCCESS, "message": "success"} + + response = sync_construct_response() + + mock_jsonify.assert_called_once_with({"code": RetCode.SUCCESS, "message": "success"}) + assert response.headers["Access-Control-Allow-Origin"] == "*" + assert response.headers["Access-Control-Allow-Method"] == "*" + assert response.headers["Access-Control-Allow-Headers"] == "*" + assert response.headers["Access-Control-Expose-Headers"] == "Authorization" + + def test_sync_construct_response_with_data(self): + """Test sync_construct_response with data parameter""" + with patch('flask.make_response') as mock_make_response: + with patch('flask.jsonify') as mock_jsonify: + mock_response = MagicMock() + mock_response.headers = {} + mock_make_response.return_value = mock_response + + test_data = {"result": "test"} + response = sync_construct_response(data=test_data) + + call_args = mock_jsonify.call_args[0][0] + assert call_args["code"] == RetCode.SUCCESS + assert call_args["message"] == "success" + assert call_args["data"] == test_data + + def test_sync_construct_response_with_error_code(self): + """Test sync_construct_response with error code""" + with patch('flask.make_response') as mock_make_response: + with patch('flask.jsonify') as mock_jsonify: + mock_response = MagicMock() + mock_response.headers = {} + mock_make_response.return_value = mock_response + + response = sync_construct_response( + code=RetCode.SERVER_ERROR, + message="Server error occurred" + ) + + call_args = mock_jsonify.call_args[0][0] + assert call_args["code"] == RetCode.SERVER_ERROR + assert call_args["message"] == "Server error occurred" + + def test_sync_construct_response_with_auth(self): + """Test sync_construct_response with auth token""" + with patch('flask.make_response') as mock_make_response: + with patch('flask.jsonify') as mock_jsonify: + mock_response = MagicMock() + mock_response.headers = {} + mock_make_response.return_value = mock_response + + auth_token = "Bearer sync_token" + response = sync_construct_response(auth=auth_token) + + assert response.headers["Authorization"] == auth_token + + def test_sync_construct_response_none_values_excluded(self): + """Test that None values are excluded from response (except code)""" + with patch('flask.make_response') as mock_make_response: + with patch('flask.jsonify') as mock_jsonify: + mock_response = MagicMock() + mock_response.headers = {} + mock_make_response.return_value = mock_response + + response = sync_construct_response( + code=RetCode.SUCCESS, + message=None, + data=None + ) + + call_args = mock_jsonify.call_args[0][0] + assert "code" in call_args + assert "message" not in call_args + assert "data" not in call_args diff --git a/test/unit_test/common/test_http_client.py b/test/unit_test/common/test_http_client.py new file mode 100644 index 00000000000..7b17351190a --- /dev/null +++ b/test/unit_test/common/test_http_client.py @@ -0,0 +1,620 @@ +# +# Copyright 2025 The InfiniFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import asyncio +import os +import time +from unittest.mock import AsyncMock, MagicMock, patch, call + +import httpx +import pytest +import anyio + +from common.http_client import ( + _clean_headers, + _get_delay, + async_request, + sync_request, + DEFAULT_TIMEOUT, + DEFAULT_FOLLOW_REDIRECTS, + DEFAULT_MAX_REDIRECTS, + DEFAULT_MAX_RETRIES, + DEFAULT_BACKOFF_FACTOR, + DEFAULT_USER_AGENT, +) + + +class TestCleanHeaders: + """Test cases for _clean_headers helper function""" + + def test_clean_headers_none_input(self): + """Test _clean_headers with None headers""" + result = _clean_headers(None) + assert result is not None + assert "User-Agent" in result + assert result["User-Agent"] == DEFAULT_USER_AGENT + + def test_clean_headers_with_auth_token(self): + """Test _clean_headers with auth token""" + result = _clean_headers(None, auth_token="Bearer test_token") + assert result["Authorization"] == "Bearer test_token" + assert result["User-Agent"] == DEFAULT_USER_AGENT + + def test_clean_headers_with_custom_headers(self): + """Test _clean_headers with custom headers""" + custom_headers = {"X-Custom-Header": "custom_value", "Content-Type": "application/json"} + result = _clean_headers(custom_headers) + assert result["X-Custom-Header"] == "custom_value" + assert result["Content-Type"] == "application/json" + assert result["User-Agent"] == DEFAULT_USER_AGENT + + def test_clean_headers_merges_auth_and_custom(self): + """Test _clean_headers merges auth token and custom headers""" + custom_headers = {"X-Custom": "value"} + result = _clean_headers(custom_headers, auth_token="Bearer token") + assert result["Authorization"] == "Bearer token" + assert result["X-Custom"] == "value" + assert result["User-Agent"] == DEFAULT_USER_AGENT + + def test_clean_headers_filters_none_values(self): + """Test _clean_headers filters out None values""" + custom_headers = {"X-Valid": "value", "X-None": None} + result = _clean_headers(custom_headers) + assert "X-Valid" in result + assert "X-None" not in result + + def test_clean_headers_converts_to_string(self): + """Test _clean_headers converts header values to strings""" + custom_headers = {"X-Number": 123, "X-Bool": True} + result = _clean_headers(custom_headers) + assert result["X-Number"] == "123" + assert result["X-Bool"] == "True" + + def test_clean_headers_empty_dict(self): + """Test _clean_headers with empty dict""" + result = _clean_headers({}) + assert "User-Agent" in result + + +class TestGetDelay: + """Test cases for _get_delay helper function""" + + def test_get_delay_first_attempt(self): + """Test _get_delay for first retry attempt""" + delay = _get_delay(0.5, 0) + assert delay == 0.5 # 0.5 * 2^0 + + def test_get_delay_second_attempt(self): + """Test _get_delay for second retry attempt""" + delay = _get_delay(0.5, 1) + assert delay == 1.0 # 0.5 * 2^1 + + def test_get_delay_third_attempt(self): + """Test _get_delay for third retry attempt""" + delay = _get_delay(0.5, 2) + assert delay == 2.0 # 0.5 * 2^2 + + def test_get_delay_exponential_backoff(self): + """Test _get_delay exponential backoff calculation""" + backoff_factor = 1.0 + delays = [_get_delay(backoff_factor, i) for i in range(5)] + assert delays == [1.0, 2.0, 4.0, 8.0, 16.0] + + def test_get_delay_custom_backoff_factor(self): + """Test _get_delay with custom backoff factor""" + delay = _get_delay(2.0, 3) + assert delay == 16.0 # 2.0 * 2^3 + + +class TestAsyncRequest: + """Test cases for async_request function""" + + @pytest.mark.anyio + async def test_async_request_success(self): + """Test successful async_request""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.AsyncClient') as mock_client_class: + mock_client = AsyncMock() + mock_client.request = AsyncMock(return_value=mock_response) + mock_client_class.return_value.__aenter__.return_value = mock_client + + response = await async_request("GET", "https://example.com") + + assert response.status_code == 200 + mock_client.request.assert_called_once() + + @pytest.mark.anyio + async def test_async_request_with_custom_timeout(self): + """Test async_request with custom timeout""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.AsyncClient') as mock_client_class: + mock_client = AsyncMock() + mock_client.request = AsyncMock(return_value=mock_response) + mock_client_class.return_value.__aenter__.return_value = mock_client + + response = await async_request("GET", "https://example.com", timeout=30.0) + + mock_client_class.assert_called_once() + call_kwargs = mock_client_class.call_args[1] + assert call_kwargs["timeout"] == 30.0 + + @pytest.mark.anyio + async def test_async_request_with_headers(self): + """Test async_request with custom headers""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.AsyncClient') as mock_client_class: + mock_client = AsyncMock() + mock_client.request = AsyncMock(return_value=mock_response) + mock_client_class.return_value.__aenter__.return_value = mock_client + + custom_headers = {"X-Custom": "value"} + response = await async_request("GET", "https://example.com", headers=custom_headers) + + mock_client.request.assert_called_once() + call_kwargs = mock_client.request.call_args[1] + assert "X-Custom" in call_kwargs["headers"] + assert call_kwargs["headers"]["X-Custom"] == "value" + + @pytest.mark.anyio + async def test_async_request_with_auth_token(self): + """Test async_request with auth token""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.AsyncClient') as mock_client_class: + mock_client = AsyncMock() + mock_client.request = AsyncMock(return_value=mock_response) + mock_client_class.return_value.__aenter__.return_value = mock_client + + response = await async_request("GET", "https://example.com", auth_token="Bearer token") + + mock_client.request.assert_called_once() + call_kwargs = mock_client.request.call_args[1] + assert call_kwargs["headers"]["Authorization"] == "Bearer token" + + @pytest.mark.anyio + async def test_async_request_with_follow_redirects(self): + """Test async_request with follow_redirects parameter""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.AsyncClient') as mock_client_class: + mock_client = AsyncMock() + mock_client.request = AsyncMock(return_value=mock_response) + mock_client_class.return_value.__aenter__.return_value = mock_client + + response = await async_request("GET", "https://example.com", follow_redirects=False) + + mock_client_class.assert_called_once() + call_kwargs = mock_client_class.call_args[1] + assert call_kwargs["follow_redirects"] is False + + @pytest.mark.anyio + async def test_async_request_with_max_redirects(self): + """Test async_request with max_redirects parameter""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.AsyncClient') as mock_client_class: + mock_client = AsyncMock() + mock_client.request = AsyncMock(return_value=mock_response) + mock_client_class.return_value.__aenter__.return_value = mock_client + + response = await async_request("GET", "https://example.com", max_redirects=10) + + mock_client_class.assert_called_once() + call_kwargs = mock_client_class.call_args[1] + assert call_kwargs["max_redirects"] == 10 + + @pytest.mark.anyio + async def test_async_request_with_proxies(self): + """Test async_request with proxies parameter""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.AsyncClient') as mock_client_class: + mock_client = AsyncMock() + mock_client.request = AsyncMock(return_value=mock_response) + mock_client_class.return_value.__aenter__.return_value = mock_client + + proxies = "http://proxy.example.com:8080" + response = await async_request("GET", "https://example.com", proxies=proxies) + + mock_client_class.assert_called_once() + call_kwargs = mock_client_class.call_args[1] + assert call_kwargs["proxies"] == proxies + + @pytest.mark.anyio + async def test_async_request_retry_on_failure(self): + """Test async_request retries on failure""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.AsyncClient') as mock_client_class: + mock_client = AsyncMock() + # First two calls fail, third succeeds + mock_client.request = AsyncMock( + side_effect=[ + httpx.RequestError("Connection error"), + httpx.RequestError("Connection error"), + mock_response + ] + ) + mock_client_class.return_value.__aenter__.return_value = mock_client + + with patch('asyncio.sleep', new_callable=AsyncMock): + response = await async_request("GET", "https://example.com", retries=2) + + assert response.status_code == 200 + assert mock_client.request.call_count == 3 + + @pytest.mark.anyio + async def test_async_request_exhausted_retries(self): + """Test async_request raises exception when retries exhausted""" + with patch('httpx.AsyncClient') as mock_client_class: + mock_client = AsyncMock() + mock_client.request = AsyncMock(side_effect=httpx.RequestError("Connection error")) + mock_client_class.return_value.__aenter__.return_value = mock_client + + with patch('asyncio.sleep', new_callable=AsyncMock): + with pytest.raises(httpx.RequestError): + await async_request("GET", "https://example.com", retries=2) + + assert mock_client.request.call_count == 3 # Initial + 2 retries + + @pytest.mark.anyio + async def test_async_request_zero_retries(self): + """Test async_request with zero retries""" + with patch('httpx.AsyncClient') as mock_client_class: + mock_client = AsyncMock() + mock_client.request = AsyncMock(side_effect=httpx.RequestError("Connection error")) + mock_client_class.return_value.__aenter__.return_value = mock_client + + with pytest.raises(httpx.RequestError): + await async_request("GET", "https://example.com", retries=0) + + assert mock_client.request.call_count == 1 # Only initial attempt + + @pytest.mark.anyio + async def test_async_request_custom_backoff(self): + """Test async_request with custom backoff factor""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.AsyncClient') as mock_client_class: + mock_client = AsyncMock() + mock_client.request = AsyncMock( + side_effect=[httpx.RequestError("Error"), mock_response] + ) + mock_client_class.return_value.__aenter__.return_value = mock_client + + with patch('asyncio.sleep', new_callable=AsyncMock) as mock_sleep: + response = await async_request( + "GET", "https://example.com", + retries=1, + backoff_factor=2.0 + ) + + # Check that sleep was called with correct delay (2.0 * 2^0 = 2.0) + mock_sleep.assert_called_once() + assert mock_sleep.call_args[0][0] == 2.0 + + @pytest.mark.anyio + async def test_async_request_with_json_data(self): + """Test async_request with JSON data""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.AsyncClient') as mock_client_class: + mock_client = AsyncMock() + mock_client.request = AsyncMock(return_value=mock_response) + mock_client_class.return_value.__aenter__.return_value = mock_client + + json_data = {"key": "value"} + response = await async_request("POST", "https://example.com", json=json_data) + + mock_client.request.assert_called_once() + call_kwargs = mock_client.request.call_args[1] + assert call_kwargs["json"] == json_data + + @pytest.mark.anyio + async def test_async_request_post_method(self): + """Test async_request with POST method""" + mock_response = MagicMock() + mock_response.status_code = 201 + + with patch('httpx.AsyncClient') as mock_client_class: + mock_client = AsyncMock() + mock_client.request = AsyncMock(return_value=mock_response) + mock_client_class.return_value.__aenter__.return_value = mock_client + + response = await async_request("POST", "https://example.com", json={"data": "test"}) + + assert response.status_code == 201 + call_args = mock_client.request.call_args + assert call_args[1]["method"] == "POST" + + +class TestSyncRequest: + """Test cases for sync_request function""" + + def test_sync_request_success(self): + """Test successful sync_request""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.Client') as mock_client_class: + mock_client = MagicMock() + mock_client.request = MagicMock(return_value=mock_response) + mock_client_class.return_value.__enter__.return_value = mock_client + + response = sync_request("GET", "https://example.com") + + assert response.status_code == 200 + mock_client.request.assert_called_once() + + def test_sync_request_with_custom_timeout(self): + """Test sync_request with custom timeout""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.Client') as mock_client_class: + mock_client = MagicMock() + mock_client.request = MagicMock(return_value=mock_response) + mock_client_class.return_value.__enter__.return_value = mock_client + + response = sync_request("GET", "https://example.com", timeout=60.0) + + mock_client_class.assert_called_once() + call_kwargs = mock_client_class.call_args[1] + assert call_kwargs["timeout"] == 60.0 + + def test_sync_request_with_headers(self): + """Test sync_request with custom headers""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.Client') as mock_client_class: + mock_client = MagicMock() + mock_client.request = MagicMock(return_value=mock_response) + mock_client_class.return_value.__enter__.return_value = mock_client + + custom_headers = {"X-Test": "test_value"} + response = sync_request("GET", "https://example.com", headers=custom_headers) + + mock_client.request.assert_called_once() + call_kwargs = mock_client.request.call_args[1] + assert "X-Test" in call_kwargs["headers"] + assert call_kwargs["headers"]["X-Test"] == "test_value" + + def test_sync_request_with_auth_token(self): + """Test sync_request with auth token""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.Client') as mock_client_class: + mock_client = MagicMock() + mock_client.request = MagicMock(return_value=mock_response) + mock_client_class.return_value.__enter__.return_value = mock_client + + response = sync_request("GET", "https://example.com", auth_token="Bearer sync_token") + + mock_client.request.assert_called_once() + call_kwargs = mock_client.request.call_args[1] + assert call_kwargs["headers"]["Authorization"] == "Bearer sync_token" + + def test_sync_request_with_follow_redirects(self): + """Test sync_request with follow_redirects parameter""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.Client') as mock_client_class: + mock_client = MagicMock() + mock_client.request = MagicMock(return_value=mock_response) + mock_client_class.return_value.__enter__.return_value = mock_client + + response = sync_request("GET", "https://example.com", follow_redirects=False) + + mock_client_class.assert_called_once() + call_kwargs = mock_client_class.call_args[1] + assert call_kwargs["follow_redirects"] is False + + def test_sync_request_with_max_redirects(self): + """Test sync_request with max_redirects parameter""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.Client') as mock_client_class: + mock_client = MagicMock() + mock_client.request = MagicMock(return_value=mock_response) + mock_client_class.return_value.__enter__.return_value = mock_client + + response = sync_request("GET", "https://example.com", max_redirects=5) + + mock_client_class.assert_called_once() + call_kwargs = mock_client_class.call_args[1] + assert call_kwargs["max_redirects"] == 5 + + def test_sync_request_with_proxies(self): + """Test sync_request with proxies parameter""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.Client') as mock_client_class: + mock_client = MagicMock() + mock_client.request = MagicMock(return_value=mock_response) + mock_client_class.return_value.__enter__.return_value = mock_client + + proxies = "http://proxy.example.com:8080" + response = sync_request("GET", "https://example.com", proxies=proxies) + + mock_client_class.assert_called_once() + call_kwargs = mock_client_class.call_args[1] + assert call_kwargs["proxies"] == proxies + + def test_sync_request_retry_on_failure(self): + """Test sync_request retries on failure""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.Client') as mock_client_class: + mock_client = MagicMock() + # First two calls fail, third succeeds + mock_client.request = MagicMock( + side_effect=[ + httpx.RequestError("Connection error"), + httpx.RequestError("Connection error"), + mock_response + ] + ) + mock_client_class.return_value.__enter__.return_value = mock_client + + with patch('time.sleep'): + response = sync_request("GET", "https://example.com", retries=2) + + assert response.status_code == 200 + assert mock_client.request.call_count == 3 + + def test_sync_request_exhausted_retries(self): + """Test sync_request raises exception when retries exhausted""" + with patch('httpx.Client') as mock_client_class: + mock_client = MagicMock() + mock_client.request = MagicMock(side_effect=httpx.RequestError("Connection error")) + mock_client_class.return_value.__enter__.return_value = mock_client + + with patch('time.sleep'): + with pytest.raises(httpx.RequestError): + sync_request("GET", "https://example.com", retries=2) + + assert mock_client.request.call_count == 3 # Initial + 2 retries + + def test_sync_request_zero_retries(self): + """Test sync_request with zero retries""" + with patch('httpx.Client') as mock_client_class: + mock_client = MagicMock() + mock_client.request = MagicMock(side_effect=httpx.RequestError("Connection error")) + mock_client_class.return_value.__enter__.return_value = mock_client + + with pytest.raises(httpx.RequestError): + sync_request("GET", "https://example.com", retries=0) + + assert mock_client.request.call_count == 1 # Only initial attempt + + def test_sync_request_custom_backoff(self): + """Test sync_request with custom backoff factor""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.Client') as mock_client_class: + mock_client = MagicMock() + mock_client.request = MagicMock( + side_effect=[httpx.RequestError("Error"), mock_response] + ) + mock_client_class.return_value.__enter__.return_value = mock_client + + with patch('time.sleep') as mock_sleep: + response = sync_request( + "GET", "https://example.com", + retries=1, + backoff_factor=3.0 + ) + + # Check that sleep was called with correct delay (3.0 * 2^0 = 3.0) + mock_sleep.assert_called_once() + assert mock_sleep.call_args[0][0] == 3.0 + + def test_sync_request_with_json_data(self): + """Test sync_request with JSON data""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.Client') as mock_client_class: + mock_client = MagicMock() + mock_client.request = MagicMock(return_value=mock_response) + mock_client_class.return_value.__enter__.return_value = mock_client + + json_data = {"test": "data"} + response = sync_request("POST", "https://example.com", json=json_data) + + mock_client.request.assert_called_once() + call_kwargs = mock_client.request.call_args[1] + assert call_kwargs["json"] == json_data + + def test_sync_request_put_method(self): + """Test sync_request with PUT method""" + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch('httpx.Client') as mock_client_class: + mock_client = MagicMock() + mock_client.request = MagicMock(return_value=mock_response) + mock_client_class.return_value.__enter__.return_value = mock_client + + response = sync_request("PUT", "https://example.com", json={"update": "data"}) + + assert response.status_code == 200 + call_args = mock_client.request.call_args + assert call_args[1]["method"] == "PUT" + + def test_sync_request_delete_method(self): + """Test sync_request with DELETE method""" + mock_response = MagicMock() + mock_response.status_code = 204 + + with patch('httpx.Client') as mock_client_class: + mock_client = MagicMock() + mock_client.request = MagicMock(return_value=mock_response) + mock_client_class.return_value.__enter__.return_value = mock_client + + response = sync_request("DELETE", "https://example.com/resource/123") + + assert response.status_code == 204 + call_args = mock_client.request.call_args + assert call_args[1]["method"] == "DELETE" + + +class TestDefaultConstants: + """Test cases for default constants""" + + def test_default_timeout(self): + """Test DEFAULT_TIMEOUT is set correctly""" + assert DEFAULT_TIMEOUT == 15.0 or isinstance(DEFAULT_TIMEOUT, float) + + def test_default_follow_redirects(self): + """Test DEFAULT_FOLLOW_REDIRECTS is set correctly""" + assert isinstance(DEFAULT_FOLLOW_REDIRECTS, bool) + + def test_default_max_redirects(self): + """Test DEFAULT_MAX_REDIRECTS is set correctly""" + assert DEFAULT_MAX_REDIRECTS == 30 or isinstance(DEFAULT_MAX_REDIRECTS, int) + + def test_default_max_retries(self): + """Test DEFAULT_MAX_RETRIES is set correctly""" + assert DEFAULT_MAX_RETRIES == 2 or isinstance(DEFAULT_MAX_RETRIES, int) + + def test_default_backoff_factor(self): + """Test DEFAULT_BACKOFF_FACTOR is set correctly""" + assert DEFAULT_BACKOFF_FACTOR == 0.5 or isinstance(DEFAULT_BACKOFF_FACTOR, float) + + def test_default_user_agent(self): + """Test DEFAULT_USER_AGENT is set correctly""" + assert DEFAULT_USER_AGENT == "ragflow-http-client" or isinstance(DEFAULT_USER_AGENT, str)