|
|
@@ -0,0 +1,187 @@
|
|
|
+"""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)]
|