| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- """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.
- Cache mechanics (TTL, inflight dedup, suspect retry) are covered directly in
- test_month_cache.py; these tests verify each client's wiring — that the month
- caches are hooked up with eviction of the derived per-date DayInfo cache.
- """
- 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)
- def _seed(cache, key, value, age=0.0):
- cache._data[key] = value
- cache._fetched_at[key] = time.monotonic() + age
- class TestAirbnbMonthTTL:
- async def test_fresh_month_served_from_cache(self):
- client = _airbnb_client()
- _seed(client._months, (7, 2026), _days())
- calls = []
- async def _fetch(m, y):
- calls.append((m, y))
- return _days()
- client._fetch_month_days = _fetch
- await client._months.get(7, 2026)
- assert calls == []
- async def test_expired_month_refetches(self):
- client = _airbnb_client()
- _seed(client._months, (7, 2026), _days(), age=_EXPIRED)
- calls = []
- async def _fetch(m, y):
- calls.append((m, y))
- return _days(booked=True)
- client._fetch_month_days = _fetch
- result = await client._months.get(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()
- client._months._data[(7, 2026)] = _days()
- calls = []
- async def _fetch(m, y):
- calls.append((m, y))
- return _days()
- client._fetch_month_days = _fetch
- await client._months.get(7, 2026)
- assert calls == [(7, 2026)]
- async def test_expired_month_evicts_dayinfo_cache(self):
- client = _airbnb_client()
- key = (7, 2026)
- _seed(client._months, key, _days(), age=_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._months.get(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._months.get(7, 2026)
- assert key in client._months._fetched_at
- assert time.monotonic() - client._months._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._details.set('C1', {'cached': True})
- 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._details._data['C1'] = {'cached': True}
- client._details._fetched_at['C1'] = time.monotonic() + _EXPIRED
- self._stub_gql(client, {'fresh': True})
- assert await client._fetch_one_detail('C1') == {'fresh': True}
- assert client._details.get('C1') == {'fresh': True}
- class TestBookingMonthTTL:
- async def test_fresh_month_served_from_cache(self):
- client = _booking_client()
- _seed(client._months, (7, 2026), {'2026-07-10': _day_info()})
- calls = []
- async def _fetch(m, y):
- calls.append((m, y))
- return {}
- client._fetch_month_calendar = _fetch
- await client._months.get(7, 2026)
- assert calls == []
- async def test_expired_month_refetches_and_evicts_dayinfo_cache(self):
- client = _booking_client()
- key = (7, 2026)
- _seed(client._months, key, {'2026-07-10': _day_info()}, age=_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._months.get(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._months._fetched_at
- async def test_missing_timestamp_treated_as_expired(self):
- client = _booking_client()
- client._months._data[(7, 2026)] = {'2026-07-10': _day_info()}
- calls = []
- async def _fetch(m, y):
- calls.append((m, y))
- return {'2026-07-10': _day_info()} # priced result — must not trip the suspect retry
- client._fetch_month_calendar = _fetch
- await client._months.get(7, 2026)
- assert calls == [(7, 2026)]
|