test_api_client.py•13.6 kB
"""Unit tests for KYC API client."""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
from src.clients.kyc_api_client import (
KYCAPIClient,
APIError,
ValidationError,
ServiceUnavailableError,
)
class TestKYCAPIClient:
"""Test suite for KYC API client."""
def test_initialization(self):
"""Test client initialization."""
client = KYCAPIClient(
base_url="https://api.test.com",
api_key="test_key",
jwt_token="test_token",
timeout=30,
max_connections=100
)
assert client.base_url == "https://api.test.com"
assert client.api_key == "test_key"
assert client.jwt_token == "test_token"
assert client.timeout == 30
@pytest.mark.asyncio
async def test_post_success(self, mock_api_client):
"""Test successful POST request."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": {"result": "success"}}
mock_response.raise_for_status = MagicMock()
mock_api_client.client.post = AsyncMock(return_value=mock_response)
result = await mock_api_client.post(
endpoint="/test",
data={"param": "value"}
)
assert result == {"data": {"result": "success"}}
mock_api_client.client.post.assert_called_once()
@pytest.mark.asyncio
async def test_post_with_custom_headers(self, mock_api_client):
"""Test POST request with custom headers."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": {}}
mock_response.raise_for_status = MagicMock()
mock_api_client.client.post = AsyncMock(return_value=mock_response)
custom_headers = {"X-Custom": "value"}
await mock_api_client.post(
endpoint="/test",
data={},
headers=custom_headers
)
call_args = mock_api_client.client.post.call_args
assert call_args[1]["headers"] == custom_headers
@pytest.mark.asyncio
async def test_post_validation_error_422(self, mock_api_client):
"""Test handling of 422 validation error."""
mock_response = MagicMock()
mock_response.status_code = 422
mock_response.json.return_value = {
"message": "Validation failed",
"errors": [{"field": "pan", "message": "Invalid format"}]
}
request = MagicMock()
request.url = "https://api.test.com/test"
request.method = "POST"
mock_response.request = request
error = httpx.HTTPStatusError(
message="422",
request=request,
response=mock_response
)
mock_response.raise_for_status = MagicMock(side_effect=error)
mock_api_client.client.post = AsyncMock(return_value=mock_response)
with pytest.raises(ValidationError):
await mock_api_client.post(endpoint="/test", data={})
@pytest.mark.asyncio
async def test_post_service_unavailable_503(self, mock_api_client):
"""Test handling of 503 service unavailable error."""
mock_response = MagicMock()
mock_response.status_code = 503
mock_response.json.return_value = {"message": "Service unavailable"}
request = MagicMock()
request.url = "https://api.test.com/test"
request.method = "POST"
mock_response.request = request
error = httpx.HTTPStatusError(
message="503",
request=request,
response=mock_response
)
mock_response.raise_for_status = MagicMock(side_effect=error)
mock_api_client.client.post = AsyncMock(return_value=mock_response)
with pytest.raises(ServiceUnavailableError, match="temporarily unavailable"):
await mock_api_client.post(endpoint="/test", data={})
@pytest.mark.asyncio
async def test_post_server_error_500(self, mock_api_client):
"""Test handling of 500 server error."""
mock_response = MagicMock()
mock_response.status_code = 500
mock_response.json.return_value = {"message": "Internal server error"}
request = MagicMock()
request.url = "https://api.test.com/test"
request.method = "POST"
mock_response.request = request
error = httpx.HTTPStatusError(
message="500",
request=request,
response=mock_response
)
mock_response.raise_for_status = MagicMock(side_effect=error)
mock_api_client.client.post = AsyncMock(return_value=mock_response)
with pytest.raises(ServiceUnavailableError, match="Server error"):
await mock_api_client.post(endpoint="/test", data={})
@pytest.mark.asyncio
async def test_post_unauthorized_401(self, mock_api_client):
"""Test handling of 401 unauthorized error."""
mock_response = MagicMock()
mock_response.status_code = 401
mock_response.json.return_value = {"message": "Unauthorized"}
request = MagicMock()
request.url = "https://api.test.com/test"
request.method = "POST"
mock_response.request = request
error = httpx.HTTPStatusError(
message="401",
request=request,
response=mock_response
)
mock_response.raise_for_status = MagicMock(side_effect=error)
mock_api_client.client.post = AsyncMock(return_value=mock_response)
with pytest.raises(APIError, match="401"):
await mock_api_client.post(endpoint="/test", data={})
@pytest.mark.asyncio
async def test_post_request_error(self, mock_api_client):
"""Test handling of request errors (network issues)."""
mock_api_client.client.post = AsyncMock(
side_effect=httpx.RequestError("Connection failed")
)
with pytest.raises(APIError, match="Request failed"):
await mock_api_client.post(endpoint="/test", data={})
@pytest.mark.asyncio
async def test_get_success(self, mock_api_client):
"""Test successful GET request."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": {"result": "success"}}
mock_response.raise_for_status = MagicMock()
mock_api_client.client.get = AsyncMock(return_value=mock_response)
result = await mock_api_client.get(
endpoint="/test",
params={"id": "123"}
)
assert result == {"data": {"result": "success"}}
mock_api_client.client.get.assert_called_once()
@pytest.mark.asyncio
async def test_get_with_params(self, mock_api_client):
"""Test GET request with query parameters."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": {}}
mock_response.raise_for_status = MagicMock()
mock_api_client.client.get = AsyncMock(return_value=mock_response)
params = {"id": "123", "type": "test"}
await mock_api_client.get(endpoint="/test", params=params)
call_args = mock_api_client.client.get.call_args
assert call_args[1]["params"] == params
@pytest.mark.asyncio
async def test_get_error_handling(self, mock_api_client):
"""Test GET request error handling."""
mock_response = MagicMock()
mock_response.status_code = 404
mock_response.json.return_value = {"message": "Not found"}
request = MagicMock()
request.url = "https://api.test.com/test"
request.method = "GET"
mock_response.request = request
error = httpx.HTTPStatusError(
message="404",
request=request,
response=mock_response
)
mock_response.raise_for_status = MagicMock(side_effect=error)
mock_api_client.client.get = AsyncMock(return_value=mock_response)
with pytest.raises(APIError):
await mock_api_client.get(endpoint="/test")
@pytest.mark.asyncio
async def test_close(self, mock_api_client):
"""Test closing the client."""
mock_api_client.client.aclose = AsyncMock()
await mock_api_client.close()
mock_api_client.client.aclose.assert_called_once()
def test_map_error_422(self, mock_api_client):
"""Test error mapping for 422 status."""
mock_response = MagicMock()
mock_response.status_code = 422
mock_response.json.return_value = {"message": "Validation error"}
request = MagicMock()
mock_response.request = request
error = httpx.HTTPStatusError(
message="422",
request=request,
response=mock_response
)
mapped = mock_api_client._map_error(error)
assert isinstance(mapped, ValidationError)
def test_map_error_503(self, mock_api_client):
"""Test error mapping for 503 status."""
mock_response = MagicMock()
mock_response.status_code = 503
mock_response.json.return_value = {"message": "Service unavailable"}
request = MagicMock()
mock_response.request = request
error = httpx.HTTPStatusError(
message="503",
request=request,
response=mock_response
)
mapped = mock_api_client._map_error(error)
assert isinstance(mapped, ServiceUnavailableError)
def test_map_error_500_range(self, mock_api_client):
"""Test error mapping for 500+ status codes."""
for status_code in [500, 501, 502, 504]:
mock_response = MagicMock()
mock_response.status_code = status_code
mock_response.json.return_value = {"message": "Server error"}
request = MagicMock()
mock_response.request = request
error = httpx.HTTPStatusError(
message=str(status_code),
request=request,
response=mock_response
)
mapped = mock_api_client._map_error(error)
assert isinstance(mapped, ServiceUnavailableError)
def test_map_error_generic(self, mock_api_client):
"""Test error mapping for generic errors."""
mock_response = MagicMock()
mock_response.status_code = 400
mock_response.json.return_value = {"message": "Bad request"}
request = MagicMock()
mock_response.request = request
error = httpx.HTTPStatusError(
message="400",
request=request,
response=mock_response
)
mapped = mock_api_client._map_error(error)
assert isinstance(mapped, APIError)
assert "400" in str(mapped)
def test_map_error_invalid_json_response(self, mock_api_client):
"""Test error mapping when response JSON is invalid."""
mock_response = MagicMock()
mock_response.status_code = 400
mock_response.json.side_effect = Exception("Invalid JSON")
request = MagicMock()
mock_response.request = request
error = httpx.HTTPStatusError(
message="400",
request=request,
response=mock_response
)
# Should handle JSON parsing error gracefully
mapped = mock_api_client._map_error(error)
assert isinstance(mapped, APIError)
@pytest.mark.asyncio
async def test_retry_on_failure(self):
"""Test that requests are retried on failure."""
client = KYCAPIClient(
base_url="https://api.test.com",
api_key="test_key",
jwt_token="test_token"
)
# Mock to fail twice then succeed
call_count = 0
async def mock_post(*args, **kwargs):
nonlocal call_count
call_count += 1
if call_count < 3:
raise httpx.RequestError("Temporary failure")
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": "success"}
mock_response.raise_for_status = MagicMock()
return mock_response
client.client.post = mock_post
result = await client.post(endpoint="/test", data={})
assert result == {"data": "success"}
assert call_count == 3 # Failed twice, succeeded on third attempt
@pytest.mark.asyncio
async def test_retry_exhaustion(self):
"""Test that retries are exhausted after max attempts."""
client = KYCAPIClient(
base_url="https://api.test.com",
api_key="test_key",
jwt_token="test_token"
)
# Mock to always fail
client.client.post = AsyncMock(
side_effect=httpx.RequestError("Persistent failure")
)
with pytest.raises(APIError):
await client.post(endpoint="/test", data={})