""" Unit tests for cache validity checks. Tests the is_cache_valid() function which determines if cached price data is still current or needs to be refreshed. """ from __future__ import annotations from datetime import datetime from unittest.mock import Mock from zoneinfo import ZoneInfo import pytest from custom_components.tibber_prices.coordinator.cache import ( TibberPricesCacheData, is_cache_valid, ) @pytest.mark.unit def test_cache_valid_same_day() -> None: """ Test cache is valid when data is from the same calendar day. Scenario: Cache from 10:00, current time 15:00 (same day) Expected: Cache is valid """ time_service = Mock() cache_time = datetime(2025, 11, 22, 10, 0, 0, tzinfo=ZoneInfo("Europe/Oslo")) current_time = datetime(2025, 11, 22, 15, 0, 0, tzinfo=ZoneInfo("Europe/Oslo")) time_service.now.return_value = current_time time_service.as_local.side_effect = lambda dt: dt cache_data = TibberPricesCacheData( price_data={"price_info": [1, 2, 3]}, user_data={"viewer": {"home": {"id": "test"}}}, last_price_update=cache_time, last_user_update=cache_time, last_midnight_check=None, ) result = is_cache_valid(cache_data, "[TEST]", time=time_service) assert result is True @pytest.mark.unit def test_cache_invalid_different_day() -> None: """ Test cache is invalid when data is from a different calendar day. Scenario: Cache from yesterday, current time today Expected: Cache is invalid (date mismatch) """ time_service = Mock() cache_time = datetime(2025, 11, 21, 23, 50, 0, tzinfo=ZoneInfo("Europe/Oslo")) current_time = datetime(2025, 11, 22, 0, 10, 0, tzinfo=ZoneInfo("Europe/Oslo")) time_service.now.return_value = current_time time_service.as_local.side_effect = lambda dt: dt cache_data = TibberPricesCacheData( price_data={"price_info": [1, 2, 3]}, user_data={"viewer": {"home": {"id": "test"}}}, last_price_update=cache_time, last_user_update=cache_time, last_midnight_check=None, ) result = is_cache_valid(cache_data, "[TEST]", time=time_service) assert result is False @pytest.mark.unit def test_cache_invalid_no_price_data() -> None: """ Test cache is invalid when no price data exists. Scenario: Cache exists but price_data is None Expected: Cache is invalid """ time_service = Mock() current_time = datetime(2025, 11, 22, 15, 0, 0, tzinfo=ZoneInfo("Europe/Oslo")) time_service.now.return_value = current_time time_service.as_local.side_effect = lambda dt: dt cache_data = TibberPricesCacheData( price_data=None, # No price data! user_data={"viewer": {"home": {"id": "test"}}}, last_price_update=current_time, last_user_update=current_time, last_midnight_check=None, ) result = is_cache_valid(cache_data, "[TEST]", time=time_service) assert result is False @pytest.mark.unit def test_cache_invalid_no_last_update() -> None: """ Test cache is invalid when last_price_update is None. Scenario: Cache has data but no update timestamp Expected: Cache is invalid """ time_service = Mock() current_time = datetime(2025, 11, 22, 15, 0, 0, tzinfo=ZoneInfo("Europe/Oslo")) time_service.now.return_value = current_time time_service.as_local.side_effect = lambda dt: dt cache_data = TibberPricesCacheData( price_data={"price_info": [1, 2, 3]}, user_data={"viewer": {"home": {"id": "test"}}}, last_price_update=None, # No timestamp! last_user_update=None, last_midnight_check=None, ) result = is_cache_valid(cache_data, "[TEST]", time=time_service) assert result is False @pytest.mark.unit def test_cache_valid_after_midnight_turnover() -> None: """ Test cache validity after midnight turnover with updated timestamp. Scenario: Midnight turnover occurred, _last_price_update was updated to new day Expected: Cache is valid (same date as current) This tests the fix for the "date_mismatch" bug where cache appeared invalid after midnight despite successful data rotation. """ time_service = Mock() # After midnight turnover, _last_price_update should be set to current time turnover_time = datetime(2025, 11, 22, 0, 0, 5, tzinfo=ZoneInfo("Europe/Oslo")) current_time = datetime(2025, 11, 22, 0, 10, 0, tzinfo=ZoneInfo("Europe/Oslo")) time_service.now.return_value = current_time time_service.as_local.side_effect = lambda dt: dt cache_data = TibberPricesCacheData( price_data={"price_info": [1, 2]}, user_data={"viewer": {"home": {"id": "test"}}}, last_price_update=turnover_time, # Updated during turnover! last_user_update=turnover_time, last_midnight_check=turnover_time, ) result = is_cache_valid(cache_data, "[TEST]", time=time_service) assert result is True @pytest.mark.unit def test_cache_invalid_midnight_crossing_without_update() -> None: """ Test cache becomes invalid at midnight if timestamp not updated. Scenario: HA restarted after midnight, cache still has yesterday's timestamp Expected: Cache is invalid (would be caught and refreshed) """ time_service = Mock() cache_time = datetime(2025, 11, 21, 23, 55, 0, tzinfo=ZoneInfo("Europe/Oslo")) current_time = datetime(2025, 11, 22, 0, 5, 0, tzinfo=ZoneInfo("Europe/Oslo")) time_service.now.return_value = current_time time_service.as_local.side_effect = lambda dt: dt cache_data = TibberPricesCacheData( price_data={"price_info": [1, 2, 3]}, user_data={"viewer": {"home": {"id": "test"}}}, last_price_update=cache_time, # Still yesterday! last_user_update=cache_time, last_midnight_check=None, ) result = is_cache_valid(cache_data, "[TEST]", time=time_service) assert result is False @pytest.mark.unit def test_cache_validity_timezone_aware() -> None: """ Test cache validity uses local timezone for date comparison. Scenario: UTC midnight vs local timezone midnight (different dates) Expected: Comparison done in local timezone, not UTC This ensures that midnight turnover happens at local midnight, not UTC midnight. """ time_service = Mock() # 23:00 UTC on Nov 21 = 00:00 CET on Nov 22 (UTC+1) cache_time_utc = datetime(2025, 11, 21, 23, 0, 0, tzinfo=ZoneInfo("UTC")) current_time_utc = datetime(2025, 11, 21, 23, 30, 0, tzinfo=ZoneInfo("UTC")) # Convert to local timezone (CET = UTC+1) cache_time_local = cache_time_utc.astimezone(ZoneInfo("Europe/Oslo")) # 00:00 Nov 22 current_time_local = current_time_utc.astimezone(ZoneInfo("Europe/Oslo")) # 00:30 Nov 22 time_service.now.return_value = current_time_utc time_service.as_local.return_value = current_time_local cache_data = TibberPricesCacheData( price_data={"price_info": [1, 2, 3]}, user_data={"viewer": {"home": {"id": "test"}}}, last_price_update=cache_time_utc, last_user_update=cache_time_utc, last_midnight_check=None, ) # Mock as_local for cache_time def as_local_side_effect(dt: datetime) -> datetime: if dt == cache_time_utc: return cache_time_local return current_time_local time_service.as_local.side_effect = as_local_side_effect result = is_cache_valid(cache_data, "[TEST]", time=time_service) # Both times are Nov 22 in local timezone → same date → valid assert result is True @pytest.mark.unit def test_cache_validity_exact_midnight_boundary() -> None: """ Test cache validity exactly at midnight boundary. Scenario: Cache from 23:59:59, current time 00:00:00 Expected: Cache is invalid (different calendar days) """ time_service = Mock() cache_time = datetime(2025, 11, 21, 23, 59, 59, tzinfo=ZoneInfo("Europe/Oslo")) current_time = datetime(2025, 11, 22, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo")) time_service.now.return_value = current_time time_service.as_local.side_effect = lambda dt: dt cache_data = TibberPricesCacheData( price_data={"price_info": [1, 2, 3]}, user_data={"viewer": {"home": {"id": "test"}}}, last_price_update=cache_time, last_user_update=cache_time, last_midnight_check=None, ) result = is_cache_valid(cache_data, "[TEST]", time=time_service) assert result is False