Merge d422838b4c into 7d23c3aed0
This commit is contained in:
commit
7a0a7fe7e8
2 changed files with 960 additions and 0 deletions
340
test/unit_test/common/test_connection_utils.py
Normal file
340
test/unit_test/common/test_connection_utils.py
Normal file
|
|
@ -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
|
||||
620
test/unit_test/common/test_http_client.py
Normal file
620
test/unit_test/common/test_http_client.py
Normal file
|
|
@ -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)
|
||||
Loading…
Add table
Reference in a new issue