Files
compass_backoffice/tests_original_develop/unit/test_imdb_client.py
2026-06-13 08:23:21 -03:00

289 lines
12 KiB
Python

import json
import pytest
import httpx
from unittest.mock import patch, MagicMock, AsyncMock
from src.components.clients.imdb_client import ImdbClient
from src.components.clients.exceptions.imdb_exceptions import (
ImdbClientError,
ImdbHttpError,
ImdbTimeoutError,
ImdbConnectionError,
)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def client():
return ImdbClient()
def _make_mock_response(status_code: int, json_data: dict = None, content: bytes = b"data", headers: dict = None):
"""Helper to create a mock httpx.Response."""
mock_resp = MagicMock(spec=httpx.Response)
mock_resp.status_code = status_code
mock_resp.content = content
mock_resp.headers = headers or {}
if json_data is not None:
mock_resp.json.return_value = json_data
return mock_resp
# ---------------------------------------------------------------------------
# Unit tests — helper methods
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestBuildHeaders:
def test_build_headers_without_message_id(self, client):
headers = client._build_headers(client_id="test-client")
assert headers["clientId"] == "test-client"
assert "Authorization" in headers
assert headers["Accept"] == "application/json"
assert "messageId" not in headers
def test_build_headers_with_client_id(self, client):
headers = client._build_headers(client_id="test-client")
assert headers["clientId"] == "test-client"
assert "Authorization" in headers
@pytest.mark.unit
class TestBuildUrl:
def test_build_url_appends_msisdn(self, client):
msisdn = "5511999999999"
url = client._build_url(msisdn)
assert url.endswith(msisdn)
assert msisdn in url
@pytest.mark.unit
class TestParseResponse:
def test_parse_response_valid_json(self, client):
mock_resp = MagicMock()
mock_resp.json.return_value = {
"plan": {"name": "TIM Black"},
"statusType": "Active",
"statusDescription": "Full Access",
"socialSecNo": "06252533106",
"extraField": "ignored",
}
result = client._parse_response(mock_resp)
assert result.plan == {"name": "TIM Black"}
assert result.statusType == "Active"
assert result.statusDescription == "Full Access"
assert result.socialSecNo == "06252533106"
def test_parse_response_missing_optional_keys(self, client):
mock_resp = MagicMock()
mock_resp.json.return_value = {
"plan": None,
"statusType": None,
"statusDescription": None
}
result = client._parse_response(mock_resp)
assert result.plan is None
assert result.statusType is None
assert result.statusDescription is None
assert result.socialSecNo is None
def test_parse_response_invalid_json_raises_imdb_client_error(self, client):
mock_resp = MagicMock()
mock_resp.json.side_effect = json.JSONDecodeError("error", "", 0)
with pytest.raises(ImdbClientError, match="Invalid JSON response from IMDB API"):
client._parse_response(mock_resp)
# ---------------------------------------------------------------------------
# Unit tests — get_imdb_access_data (async)
# ---------------------------------------------------------------------------
@pytest.mark.unit
@pytest.mark.asyncio
class TestGetImdbAccessData:
@patch("src.components.clients.imdb_client.traced_async_client")
async def test_success_returns_parsed_data(self, mock_async_client_cls, client):
# Arrange
json_data = {"plan": {"name": "TIM Black"}, "statusType": "Active", "statusDescription": "Full Access"}
mock_resp = _make_mock_response(200, json_data=json_data, headers={"Messageid": "msg-123"})
mock_resp.raise_for_status = MagicMock()
mock_ctx = AsyncMock()
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
mock_ctx.__aexit__ = AsyncMock(return_value=False)
mock_ctx.get = AsyncMock(return_value=mock_resp)
mock_async_client_cls.return_value = mock_ctx
# Act
result = await client.get_imdb_access_data(msisdn="5511999999999", client_id="abc")
# Assert
assert result.plan == {"name": "TIM Black"}
assert result.statusType == "Active"
assert result.statusDescription == "Full Access"
@patch("src.components.clients.imdb_client.traced_async_client")
async def test_204_no_content_returns_none(self, mock_async_client_cls, client):
# Arrange
mock_resp = _make_mock_response(204, content=b"")
mock_resp.content = b""
mock_ctx = AsyncMock()
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
mock_ctx.__aexit__ = AsyncMock(return_value=False)
mock_ctx.get = AsyncMock(return_value=mock_resp)
mock_async_client_cls.return_value = mock_ctx
# Act
result = await client.get_imdb_access_data(msisdn="5511999999999", client_id="abc")
# Assert
assert result is None
@patch("src.components.clients.imdb_client.traced_async_client")
async def test_http_404_raises_imdb_http_error(self, mock_async_client_cls, client):
# Arrange
mock_resp = _make_mock_response(404)
mock_resp.raise_for_status.side_effect = httpx.HTTPStatusError(
"Not Found", request=MagicMock(), response=mock_resp
)
mock_ctx = AsyncMock()
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
mock_ctx.__aexit__ = AsyncMock(return_value=False)
mock_ctx.get = AsyncMock(return_value=mock_resp)
mock_async_client_cls.return_value = mock_ctx
# Act & Assert
with pytest.raises(ImdbHttpError) as exc_info:
await client.get_imdb_access_data(msisdn="5511999999999", client_id="abc")
assert exc_info.value.status_code == 404
@patch("src.components.clients.imdb_client.traced_async_client")
async def test_http_500_raises_imdb_http_error(self, mock_async_client_cls, client):
# Arrange
mock_resp = _make_mock_response(500)
mock_resp.raise_for_status.side_effect = httpx.HTTPStatusError(
"Internal Server Error", request=MagicMock(), response=mock_resp
)
mock_ctx = AsyncMock()
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
mock_ctx.__aexit__ = AsyncMock(return_value=False)
mock_ctx.get = AsyncMock(return_value=mock_resp)
mock_async_client_cls.return_value = mock_ctx
# Act & Assert
with pytest.raises(ImdbHttpError) as exc_info:
await client.get_imdb_access_data(msisdn="5511999999999", client_id="abc")
assert exc_info.value.status_code == 500
@patch("src.components.clients.imdb_client.traced_async_client")
async def test_timeout_raises_imdb_timeout_error(self, mock_async_client_cls, client):
# Arrange
mock_ctx = AsyncMock()
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
mock_ctx.__aexit__ = AsyncMock(return_value=False)
mock_ctx.get = AsyncMock(side_effect=httpx.TimeoutException("timed out"))
mock_async_client_cls.return_value = mock_ctx
# Act & Assert
with pytest.raises(ImdbTimeoutError):
await client.get_imdb_access_data(msisdn="5511999999999", client_id="abc")
@patch("src.components.clients.imdb_client.traced_async_client")
async def test_request_error_raises_imdb_connection_error(self, mock_async_client_cls, client):
# Arrange
mock_ctx = AsyncMock()
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
mock_ctx.__aexit__ = AsyncMock(return_value=False)
mock_ctx.get = AsyncMock(side_effect=httpx.RequestError("connection refused"))
mock_async_client_cls.return_value = mock_ctx
# Act & Assert
with pytest.raises(ImdbConnectionError):
await client.get_imdb_access_data(msisdn="5511999999999", client_id="abc")
@patch("src.components.clients.imdb_client.traced_async_client")
async def test_unexpected_exception_raises_imdb_client_error(self, mock_async_client_cls, client):
# Arrange
mock_ctx = AsyncMock()
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
mock_ctx.__aexit__ = AsyncMock(return_value=False)
mock_ctx.get = AsyncMock(side_effect=RuntimeError("unexpected"))
mock_async_client_cls.return_value = mock_ctx
# Act & Assert
with pytest.raises(ImdbClientError):
await client.get_imdb_access_data(msisdn="5511999999999", client_id="abc")
@pytest.mark.unit
@pytest.mark.asyncio
class TestImdbClientRetry:
@patch("src.components.clients.imdb_client.ImdbClient.get_imdb_access_data")
async def test_retry_on_connection_error_success_second_attempt(self, mock_get_data, client):
# Arrange
mock_get_data.side_effect = [
ImdbConnectionError("http://test", "timeout"),
MagicMock(plan={"name": "TIM Black"}, statusType="Active", statusDescription="Full Access")
]
# Act
with patch("src.components.clients.imdb_client.asyncio.sleep", AsyncMock()):
result = await client.get_imdb_access_data_with_retry(msisdn="5511999999999", client_id="abc")
# Assert
assert result.plan == {"name": "TIM Black"}
assert mock_get_data.call_count == 2
@patch("src.components.clients.imdb_client.ImdbClient.get_imdb_access_data")
async def test_retry_on_429_success_second_attempt(self, mock_get_data, client):
# Arrange
mock_get_data.side_effect = [
ImdbHttpError(429, "Too Many Requests"),
MagicMock(plan={"name": "TIM Black"}, statusType="Active", statusDescription="Full Access")
]
# Act
with patch("src.components.clients.imdb_client.asyncio.sleep", AsyncMock()):
result = await client.get_imdb_access_data_with_retry(msisdn="5511999999999", client_id="abc")
# Assert
assert result.plan == {"name": "TIM Black"}
assert mock_get_data.call_count == 2
@patch("src.components.clients.imdb_client.ImdbClient.get_imdb_access_data")
async def test_no_retry_on_401_error(self, mock_get_data, client):
# Arrange
mock_get_data.side_effect = ImdbHttpError(401, "Unauthorized")
# Act & Assert
with pytest.raises(ImdbHttpError) as exc_info:
await client.get_imdb_access_data_with_retry(msisdn="5511999999999", client_id="abc")
assert exc_info.value.status_code == 401
assert mock_get_data.call_count == 1
@patch("src.components.clients.imdb_client.ImdbClient.get_imdb_access_data")
async def test_exhaust_retries_raises_exceeded_error(self, mock_get_data, client):
# Arrange
last_error = ImdbTimeoutError("http://test", 5.0)
mock_get_data.side_effect = last_error
# Act & Assert
from src.components.clients.exceptions.imdb_exceptions import ImdbExceededRetriesError
with patch("src.components.clients.imdb_client.asyncio.sleep", AsyncMock()):
with pytest.raises(ImdbExceededRetriesError) as exc_info:
await client.get_imdb_access_data_with_retry(msisdn="5511999999999", client_id="abc", max_retries=2)
assert exc_info.value.attempts == 2
assert mock_get_data.call_count == 3 # Initial + 2 retries