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