fix(validation): enhance user data validation to require active subscription and price info.

Fixes #73
This commit is contained in:
Julian Pawlowski 2026-01-20 12:33:45 +00:00
parent 4b32568665
commit f88d6738e6
2 changed files with 122 additions and 25 deletions

View file

@ -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)

View file

@ -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