test_service_cache_ttl.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. """Tests for the service-client cache TTL (CACHE_TTL in service_api/_utils.py).
  2. The month/detail caches exist to dedupe requests within one operation; entries
  3. older than the TTL must be refetched so upstream changes (e.g. a canceled
  4. reservation) can't outlive them in a long-running server process.
  5. Cache mechanics (TTL, inflight dedup, suspect retry) are covered directly in
  6. test_month_cache.py; these tests verify each client's wiring — that the month
  7. caches are hooked up with eviction of the derived per-date DayInfo cache.
  8. """
  9. import time
  10. from room_cal.service_api._utils import CACHE_TTL
  11. from room_cal.service_api.types import DayInfo
  12. def _airbnb_client():
  13. from room_cal.service_api._airbnb import _AirbnbClient
  14. return _AirbnbClient(cookies='test', listing_id='12345678')
  15. def _booking_client():
  16. from room_cal.service_api._booking import _BookingClient
  17. return _BookingClient(cookies='test', session='ses', hotel_id=1,
  18. account_id=1, room_id=1, rate_id='r1')
  19. def _days(booked=False):
  20. day = {'day': '2026-07-10', 'priceData': {'price': 100}, 'available': True}
  21. if booked:
  22. day['reservationData'] = {'confirmationCode': 'TEST123'}
  23. return [day]
  24. def _day_info(d='2026-07-10'):
  25. return DayInfo(date=d, price=100, available=True, booked=False)
  26. _EXPIRED = -(CACHE_TTL + 1)
  27. def _seed(cache, key, value, age=0.0):
  28. cache._data[key] = value
  29. cache._fetched_at[key] = time.monotonic() + age
  30. class TestAirbnbMonthTTL:
  31. async def test_fresh_month_served_from_cache(self):
  32. client = _airbnb_client()
  33. _seed(client._months, (7, 2026), _days())
  34. calls = []
  35. async def _fetch(m, y):
  36. calls.append((m, y))
  37. return _days()
  38. client._fetch_month_days = _fetch
  39. await client._months.get(7, 2026)
  40. assert calls == []
  41. async def test_expired_month_refetches(self):
  42. client = _airbnb_client()
  43. _seed(client._months, (7, 2026), _days(), age=_EXPIRED)
  44. calls = []
  45. async def _fetch(m, y):
  46. calls.append((m, y))
  47. return _days(booked=True)
  48. client._fetch_month_days = _fetch
  49. result = await client._months.get(7, 2026)
  50. assert calls == [(7, 2026)]
  51. assert any(d.get('reservationData') for d in result)
  52. async def test_missing_timestamp_treated_as_expired(self):
  53. client = _airbnb_client()
  54. client._months._data[(7, 2026)] = _days()
  55. calls = []
  56. async def _fetch(m, y):
  57. calls.append((m, y))
  58. return _days()
  59. client._fetch_month_days = _fetch
  60. await client._months.get(7, 2026)
  61. assert calls == [(7, 2026)]
  62. async def test_expired_month_evicts_dayinfo_cache(self):
  63. client = _airbnb_client()
  64. key = (7, 2026)
  65. _seed(client._months, key, _days(), age=_EXPIRED)
  66. client._fetched_months.add(key)
  67. client._cache['2026-07-10'] = _day_info('2026-07-10')
  68. client._cache['2026-08-01'] = _day_info('2026-08-01') # different month — must survive
  69. async def _fetch(m, y): return _days()
  70. client._fetch_month_days = _fetch
  71. await client._months.get(7, 2026)
  72. assert key not in client._fetched_months
  73. assert '2026-07-10' not in client._cache
  74. assert '2026-08-01' in client._cache
  75. async def test_refetch_records_fresh_timestamp(self):
  76. client = _airbnb_client()
  77. key = (7, 2026)
  78. async def _fetch(m, y): return _days()
  79. client._fetch_month_days = _fetch
  80. await client._months.get(7, 2026)
  81. assert key in client._months._fetched_at
  82. assert time.monotonic() - client._months._fetched_at[key] < CACHE_TTL
  83. class TestAirbnbDetailTTL:
  84. def _stub_gql(self, client, det):
  85. async def _gql(op, h, variables, referer):
  86. return {'data': {'presentation': {'hostingDetails': {'stayHostingDetails': det}}}}
  87. client._gql = _gql
  88. async def test_fresh_detail_served_from_cache(self):
  89. client = _airbnb_client()
  90. client._details.set('C1', {'cached': True})
  91. self._stub_gql(client, {'fresh': True})
  92. assert await client._fetch_one_detail('C1') == {'cached': True}
  93. async def test_expired_detail_refetches(self):
  94. client = _airbnb_client()
  95. client._details._data['C1'] = {'cached': True}
  96. client._details._fetched_at['C1'] = time.monotonic() + _EXPIRED
  97. self._stub_gql(client, {'fresh': True})
  98. assert await client._fetch_one_detail('C1') == {'fresh': True}
  99. assert client._details.get('C1') == {'fresh': True}
  100. class TestBookingMonthTTL:
  101. async def test_fresh_month_served_from_cache(self):
  102. client = _booking_client()
  103. _seed(client._months, (7, 2026), {'2026-07-10': _day_info()})
  104. calls = []
  105. async def _fetch(m, y):
  106. calls.append((m, y))
  107. return {}
  108. client._fetch_month_calendar = _fetch
  109. await client._months.get(7, 2026)
  110. assert calls == []
  111. async def test_expired_month_refetches_and_evicts_dayinfo_cache(self):
  112. client = _booking_client()
  113. key = (7, 2026)
  114. _seed(client._months, key, {'2026-07-10': _day_info()}, age=_EXPIRED)
  115. client._cache['2026-07-10'] = _day_info('2026-07-10')
  116. client._cache['2026-08-01'] = _day_info('2026-08-01') # different month — must survive
  117. calls = []
  118. async def _fetch(m, y):
  119. calls.append((m, y))
  120. return {'2026-07-10': _day_info()}
  121. client._fetch_month_calendar = _fetch
  122. await client._months.get(7, 2026)
  123. assert calls == [(7, 2026)]
  124. assert '2026-07-10' not in client._cache
  125. assert '2026-08-01' in client._cache
  126. assert key in client._months._fetched_at
  127. async def test_missing_timestamp_treated_as_expired(self):
  128. client = _booking_client()
  129. client._months._data[(7, 2026)] = {'2026-07-10': _day_info()}
  130. calls = []
  131. async def _fetch(m, y):
  132. calls.append((m, y))
  133. return {'2026-07-10': _day_info()} # priced result — must not trip the suspect retry
  134. client._fetch_month_calendar = _fetch
  135. await client._months.get(7, 2026)
  136. assert calls == [(7, 2026)]