Model/tests/infrastructure/solar/test_google_solar_api_client.py

110 lines
3.8 KiB
Python

from typing import Any
from unittest.mock import MagicMock, patch
import pytest
import requests
from infrastructure.solar.google_solar_api_client import (
BuildingInsightsNotFoundError,
GoogleSolarApiClient,
)
# ---------------------------------------------------------------------------
# Slice 1: Successful response returns parsed JSON
# ---------------------------------------------------------------------------
def test_get_building_insights_returns_parsed_json() -> None:
# Arrange
client = GoogleSolarApiClient(api_key="test-key")
payload: dict[str, Any] = {"solarPotential": {"maxArrayPanelsCount": 20}}
mock_response = MagicMock()
mock_response.json.return_value = payload
with patch("requests.get", return_value=mock_response) as mock_get:
mock_response.raise_for_status.return_value = None
# Act
result = client.get_building_insights(longitude=-0.1278, latitude=51.5074)
# Assert
assert result == payload
mock_get.assert_called_once()
# ---------------------------------------------------------------------------
# Slice 2: Transient HTTP errors trigger retries
# ---------------------------------------------------------------------------
def test_get_building_insights_retries_on_transient_error() -> None:
# Arrange
client = GoogleSolarApiClient(api_key="test-key")
payload: dict[str, Any] = {"solarPotential": {"maxArrayPanelsCount": 20}}
transient_error = requests.exceptions.ConnectionError("timeout")
transient_error.response = None # type: ignore[attr-defined]
success_response = MagicMock()
success_response.json.return_value = payload
success_response.raise_for_status.return_value = None
with patch("requests.get", side_effect=[transient_error, success_response]) as mock_get:
with patch("time.sleep"):
# Act
result = client.get_building_insights(longitude=-0.1278, latitude=51.5074)
# Assert
assert result == payload
assert mock_get.call_count == 2
# ---------------------------------------------------------------------------
# Slice 3: 404 + entity-not-found raises BuildingInsightsNotFoundError
# ---------------------------------------------------------------------------
def test_get_building_insights_raises_on_entity_not_found() -> None:
# Arrange
client = GoogleSolarApiClient(api_key="test-key")
not_found_response = MagicMock()
not_found_response.status_code = 404
not_found_response.json.return_value = {
"error": {"message": GoogleSolarApiClient.ENTITY_NOT_FOUND_ERROR}
}
http_error = requests.exceptions.HTTPError("404")
http_error.response = not_found_response
with patch("requests.get") as mock_get:
mock_get.return_value.raise_for_status.side_effect = http_error
mock_get.return_value.response = not_found_response
# Act / Assert
with pytest.raises(BuildingInsightsNotFoundError):
client.get_building_insights(longitude=-0.1278, latitude=51.5074)
assert mock_get.call_count == 1
# ---------------------------------------------------------------------------
# Slice 4: Retry exhaustion propagates the exception to the caller
# ---------------------------------------------------------------------------
def test_get_building_insights_propagates_exception_after_retry_exhaustion() -> None:
# Arrange
client = GoogleSolarApiClient(api_key="test-key")
error = requests.exceptions.ConnectionError("persistent failure")
error.response = None # type: ignore[attr-defined]
with patch("requests.get", side_effect=error) as mock_get:
with patch("time.sleep"):
# Act / Assert
with pytest.raises(requests.exceptions.ConnectionError):
client.get_building_insights(longitude=-0.1278, latitude=51.5074)
assert mock_get.call_count == GoogleSolarApiClient.MAX_RETRIES