"""Tests for the service-client cache TTL (CACHE_TTL in service_api/_utils.py). The month/detail caches exist to dedupe requests within one operation; entries older than the TTL must be refetched so upstream changes (e.g. a canceled reservation) can't outlive them in a long-running server process. """ import time from room_cal.service_api._utils import CACHE_TTL from room_cal.service_api.types import DayInfo def _airbnb_client(): from room_cal.service_api._airbnb import _AirbnbClient return _AirbnbClient(cookies='test', listing_id='12345678') def _booking_client(): from room_cal.service_api._booking import _BookingClient return _BookingClient(cookies='test', session='ses', hotel_id=1, account_id=1, room_id=1, rate_id='r1') def _days(booked=False): day = {'day': '2026-07-10', 'priceData': {'price': 100}, 'available': True} if booked: day['reservationData'] = {'confirmationCode': 'TEST123'} return [day] def _day_info(d='2026-07-10'): return DayInfo(date=d, price=100, available=True, booked=False, rooms_to_sell=1) _EXPIRED = -(CACHE_TTL + 1) class TestAirbnbMonthTTL: async def test_fresh_month_served_from_cache(self): client = _airbnb_client() key = (7, 2026) client._raw_months[key] = _days() client._month_fetched_at[key] = time.monotonic() calls = [] async def _fetch(m, y): calls.append((m, y)) return _days() client._fetch_month_days = _fetch await client._get_month_days(7, 2026) assert calls == [] async def test_expired_month_refetches(self): client = _airbnb_client() key = (7, 2026) client._raw_months[key] = _days() client._month_fetched_at[key] = time.monotonic() + _EXPIRED calls = [] async def _fetch(m, y): calls.append((m, y)) return _days(booked=True) client._fetch_month_days = _fetch result = await client._get_month_days(7, 2026) assert calls == [(7, 2026)] assert any(d.get('reservationData') for d in result) async def test_missing_timestamp_treated_as_expired(self): client = _airbnb_client() key = (7, 2026) client._raw_months[key] = _days() calls = [] async def _fetch(m, y): calls.append((m, y)) return _days() client._fetch_month_days = _fetch await client._get_month_days(7, 2026) assert calls == [(7, 2026)] async def test_expired_month_evicts_dayinfo_cache(self): client = _airbnb_client() key = (7, 2026) client._raw_months[key] = _days() client._month_fetched_at[key] = time.monotonic() + _EXPIRED client._fetched_months.add(key) client._cache['2026-07-10'] = _day_info('2026-07-10') client._cache['2026-08-01'] = _day_info('2026-08-01') # different month — must survive async def _fetch(m, y): return _days() client._fetch_month_days = _fetch await client._get_month_days(7, 2026) assert key not in client._fetched_months assert '2026-07-10' not in client._cache assert '2026-08-01' in client._cache async def test_refetch_records_fresh_timestamp(self): client = _airbnb_client() key = (7, 2026) async def _fetch(m, y): return _days() client._fetch_month_days = _fetch await client._get_month_days(7, 2026) assert key in client._month_fetched_at assert time.monotonic() - client._month_fetched_at[key] < CACHE_TTL class TestAirbnbDetailTTL: def _stub_gql(self, client, det): async def _gql(op, h, variables, referer): return {'data': {'presentation': {'hostingDetails': {'stayHostingDetails': det}}}} client._gql = _gql async def test_fresh_detail_served_from_cache(self): client = _airbnb_client() client._detail_cache['C1'] = {'cached': True} client._detail_fetched_at['C1'] = time.monotonic() self._stub_gql(client, {'fresh': True}) assert await client._fetch_one_detail('C1') == {'cached': True} async def test_expired_detail_refetches(self): client = _airbnb_client() client._detail_cache['C1'] = {'cached': True} client._detail_fetched_at['C1'] = time.monotonic() + _EXPIRED self._stub_gql(client, {'fresh': True}) assert await client._fetch_one_detail('C1') == {'fresh': True} assert client._detail_cache['C1'] == {'fresh': True} class TestBookingMonthTTL: async def test_fresh_month_served_from_cache(self): client = _booking_client() key = (7, 2026) client._month_data[key] = {'2026-07-10': _day_info()} client._month_fetched_at[key] = time.monotonic() calls = [] async def _fetch(m, y): calls.append((m, y)) return {} client._fetch_month_calendar = _fetch await client._get_month_calendar(7, 2026) assert calls == [] async def test_expired_month_refetches_and_evicts_dayinfo_cache(self): client = _booking_client() key = (7, 2026) client._month_data[key] = {'2026-07-10': _day_info()} client._month_fetched_at[key] = time.monotonic() + _EXPIRED client._cache['2026-07-10'] = _day_info('2026-07-10') client._cache['2026-08-01'] = _day_info('2026-08-01') # different month — must survive calls = [] async def _fetch(m, y): calls.append((m, y)) return {'2026-07-10': _day_info()} client._fetch_month_calendar = _fetch await client._get_month_calendar(7, 2026) assert calls == [(7, 2026)] assert '2026-07-10' not in client._cache assert '2026-08-01' in client._cache assert key in client._month_fetched_at async def test_missing_timestamp_treated_as_expired(self): client = _booking_client() key = (7, 2026) client._month_data[key] = {'2026-07-10': _day_info()} calls = [] async def _fetch(m, y): calls.append((m, y)) return {} client._fetch_month_calendar = _fetch await client._get_month_calendar(7, 2026) assert calls == [(7, 2026)]