diff --git a/custom_components/tibber_prices/coordinator/price_data_manager.py b/custom_components/tibber_prices/coordinator/price_data_manager.py index ce739e2..8095633 100644 --- a/custom_components/tibber_prices/coordinator/price_data_manager.py +++ b/custom_components/tibber_prices/coordinator/price_data_manager.py @@ -218,21 +218,43 @@ class TibberPricesPriceDataManager: self._log("warning", "User data validation failed: Home %s missing timezone", home_id) return False - # Currency is critical - if home has subscription, must have currency + # Currency is REQUIRED - we cannot function without it + # The currency is nested in currentSubscription.priceInfo.current.currency subscription = home.get("currentSubscription") - if subscription and subscription is not None: - price_info = subscription.get("priceInfo") - if price_info and price_info is not None: - current = price_info.get("current") - if current and current is not None: - currency = current.get("currency") - if not currency: - self._log( - "warning", - "User data validation failed: Home %s has subscription but no currency", - home_id, - ) - return False + if not subscription: + self._log( + "warning", + "User data validation failed: Home %s has no active subscription", + home_id, + ) + return False + + price_info = subscription.get("priceInfo") + if not price_info: + self._log( + "warning", + "User data validation failed: Home %s subscription has no priceInfo", + home_id, + ) + return False + + current = price_info.get("current") + if not current: + self._log( + "warning", + "User data validation failed: Home %s priceInfo has no current data", + home_id, + ) + return False + + currency = current.get("currency") + if not currency: + self._log( + "warning", + "User data validation failed: Home %s has no currency", + home_id, + ) + return False break @@ -419,6 +441,10 @@ class TibberPricesPriceDataManager: """ Get currency for a specific home from cached user_data. + Note: The cached user_data is validated before storage, so if we have + cached data it should contain valid currency. This method extracts + the currency from the nested structure. + Returns: Currency code (e.g., "EUR", "NOK", "SEK"). @@ -444,7 +470,7 @@ class TibberPricesPriceDataManager: currency = current.get("currency") if not currency: - # Home without active subscription - cannot determine currency + # This should not happen if validation worked correctly msg = f"Home {home_id} has no active subscription - currency unavailable" self._log("error", msg) raise TibberPricesApiClientError(msg) diff --git a/tests/test_user_data_validation.py b/tests/test_user_data_validation.py index a4942fa..d01ef39 100644 --- a/tests/test_user_data_validation.py +++ b/tests/test_user_data_validation.py @@ -89,14 +89,21 @@ def test_validate_user_data_complete(mock_api_client, mock_time_service, mock_st } } - assert price_data_manager._validate_user_data(user_data, "home-123") is True # noqa: SLF001 # noqa: SLF001 + assert price_data_manager._validate_user_data(user_data, "home-123") is True # noqa: SLF001 @pytest.mark.unit def test_validate_user_data_none_subscription( mock_api_client: Mock, mock_time_service: Mock, mock_store: Mock, mock_interval_pool: Mock ) -> None: - """Test that user data without subscription (but with timezone) passes validation.""" + """ + Test that user data without subscription fails validation. + + Currency is required for the integration to function - if the API returns + data without a subscription, we cannot extract the currency and must reject + the data. This ensures we keep using previously cached valid data instead + of accepting incomplete API responses. + """ price_data_manager = TibberPricesPriceDataManager( api=mock_api_client, store=mock_store, @@ -119,8 +126,8 @@ def test_validate_user_data_none_subscription( } } - # Should pass validation - timezone is present, subscription being None is valid - assert price_data_manager._validate_user_data(user_data, "home-123") is True # noqa: SLF001 # noqa: SLF001 + # Should FAIL validation - subscription is required for currency + assert price_data_manager._validate_user_data(user_data, "home-123") is False # noqa: SLF001 @pytest.mark.unit @@ -217,7 +224,71 @@ def test_validate_user_data_home_not_found(mock_api_client, mock_time_service, m } } - assert price_data_manager._validate_user_data(user_data, "home-123") is False # noqa: SLF001 # noqa: SLF001 + assert price_data_manager._validate_user_data(user_data, "home-123") is False # noqa: SLF001 + + +@pytest.mark.unit +def test_validate_user_data_rejects_missing_subscription( + mock_api_client: Mock, mock_time_service: Mock, mock_store: Mock, mock_interval_pool: Mock +) -> None: + """Test that validation rejects user data when subscription is missing.""" + price_data_manager = TibberPricesPriceDataManager( + api=mock_api_client, + store=mock_store, + log_prefix="[Test]", + user_update_interval=timedelta(days=1), + time=mock_time_service, + home_id="home-123", + interval_pool=mock_interval_pool, + ) + + # User data with missing subscription (temporary API issue) + user_data = { + "viewer": { + "homes": [ + { + "id": "home-123", + "timeZone": "Europe/Berlin", + "currentSubscription": None, # No subscription - should be rejected + } + ] + } + } + + # Validation should reject this data + assert price_data_manager._validate_user_data(user_data, "home-123") is False # noqa: SLF001 + + +@pytest.mark.unit +def test_validate_user_data_rejects_missing_price_info( + mock_api_client: Mock, mock_time_service: Mock, mock_store: Mock, mock_interval_pool: Mock +) -> None: + """Test that validation rejects user data when priceInfo is missing.""" + price_data_manager = TibberPricesPriceDataManager( + api=mock_api_client, + store=mock_store, + log_prefix="[Test]", + user_update_interval=timedelta(days=1), + time=mock_time_service, + home_id="home-123", + interval_pool=mock_interval_pool, + ) + + user_data = { + "viewer": { + "homes": [ + { + "id": "home-123", + "timeZone": "Europe/Berlin", + "currentSubscription": { + "priceInfo": None, # Missing priceInfo + }, + } + ] + } + } + + assert price_data_manager._validate_user_data(user_data, "home-123") is False # noqa: SLF001 @pytest.mark.unit @@ -237,7 +308,7 @@ def test_get_currency_raises_on_no_cached_data( # No cached data with pytest.raises(TibberPricesApiClientError, match="No user data cached"): - price_data_manager._get_currency_for_home("home-123") # noqa: SLF001 # noqa: SLF001 + price_data_manager._get_currency_for_home("home-123") # noqa: SLF001 @pytest.mark.unit @@ -255,7 +326,7 @@ def test_get_currency_raises_on_no_subscription( interval_pool=mock_interval_pool, ) - price_data_manager._cached_user_data = { # noqa: SLF001 # noqa: SLF001 + price_data_manager._cached_user_data = { # noqa: SLF001 "viewer": { "homes": [ { @@ -267,7 +338,7 @@ def test_get_currency_raises_on_no_subscription( } with pytest.raises(TibberPricesApiClientError, match="has no active subscription"): - price_data_manager._get_currency_for_home("home-123") # noqa: SLF001 # noqa: SLF001 + price_data_manager._get_currency_for_home("home-123") # noqa: SLF001 @pytest.mark.unit @@ -285,7 +356,7 @@ def test_get_currency_extracts_valid_currency( interval_pool=mock_interval_pool, ) - price_data_manager._cached_user_data = { # noqa: SLF001 # noqa: SLF001 + price_data_manager._cached_user_data = { # noqa: SLF001 "viewer": { "homes": [ { @@ -302,7 +373,7 @@ def test_get_currency_extracts_valid_currency( } } - assert price_data_manager._get_currency_for_home("home-123") == "NOK" # noqa: SLF001 # noqa: SLF001 + assert price_data_manager._get_currency_for_home("home-123") == "NOK" # noqa: SLF001 @pytest.mark.unit