test_service_cache_ttl.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  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. """
  6. import time
  7. from room_cal.service_api._utils import CACHE_TTL
  8. from room_cal.service_api.types import DayInfo
  9. def _airbnb_client():
  10. from room_cal.service_api._airbnb import _AirbnbClient
  11. return _AirbnbClient(cookies='test', listing_id='12345678')
  12. def _booking_client():
  13. from room_cal.service_api._booking import _BookingClient
  14. return _BookingClient(cookies='test', session='ses', hotel_id=1,
  15. account_id=1, room_id=1, rate_id='r1')
  16. def _days(booked=False):
  17. day = {'day': '2026-07-10', 'priceData': {'price': 100}, 'available': True}
  18. if booked:
  19. day['reservationData'] = {'confirmationCode': 'TEST123'}
  20. return [day]
  21. def _day_info(d='2026-07-10'):
  22. return DayInfo(date=d, price=100, available=True, booked=False, rooms_to_sell=1)
  23. _EXPIRED = -(CACHE_TTL + 1)
  24. class TestAirbnbMonthTTL:
  25. async def test_fresh_month_served_from_cache(self):
  26. client = _airbnb_client()
  27. key = (7, 2026)
  28. client._raw_months[key] = _days()
  29. client._month_fetched_at[key] = time.monotonic()
  30. calls = []
  31. async def _fetch(m, y):
  32. calls.append((m, y))
  33. return _days()
  34. client._fetch_month_days = _fetch
  35. await client._get_month_days(7, 2026)
  36. assert calls == []
  37. async def test_expired_month_refetches(self):
  38. client = _airbnb_client()
  39. key = (7, 2026)
  40. client._raw_months[key] = _days()
  41. client._month_fetched_at[key] = time.monotonic() + _EXPIRED
  42. calls = []
  43. async def _fetch(m, y):
  44. calls.append((m, y))
  45. return _days(booked=True)
  46. client._fetch_month_days = _fetch
  47. result = await client._get_month_days(7, 2026)
  48. assert calls == [(7, 2026)]
  49. assert any(d.get('reservationData') for d in result)
  50. async def test_missing_timestamp_treated_as_expired(self):
  51. client = _airbnb_client()
  52. key = (7, 2026)
  53. client._raw_months[key] = _days()
  54. calls = []
  55. async def _fetch(m, y):
  56. calls.append((m, y))
  57. return _days()
  58. client._fetch_month_days = _fetch
  59. await client._get_month_days(7, 2026)
  60. assert calls == [(7, 2026)]
  61. async def test_expired_month_evicts_dayinfo_cache(self):
  62. client = _airbnb_client()
  63. key = (7, 2026)
  64. client._raw_months[key] = _days()
  65. client._month_fetched_at[key] = time.monotonic() + _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._get_month_days(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._get_month_days(7, 2026)
  81. assert key in client._month_fetched_at
  82. assert time.monotonic() - client._month_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._detail_cache['C1'] = {'cached': True}
  91. client._detail_fetched_at['C1'] = time.monotonic()
  92. self._stub_gql(client, {'fresh': True})
  93. assert await client._fetch_one_detail('C1') == {'cached': True}
  94. async def test_expired_detail_refetches(self):
  95. client = _airbnb_client()
  96. client._detail_cache['C1'] = {'cached': True}
  97. client._detail_fetched_at['C1'] = time.monotonic() + _EXPIRED
  98. self._stub_gql(client, {'fresh': True})
  99. assert await client._fetch_one_detail('C1') == {'fresh': True}
  100. assert client._detail_cache['C1'] == {'fresh': True}
  101. class TestBookingMonthTTL:
  102. async def test_fresh_month_served_from_cache(self):
  103. client = _booking_client()
  104. key = (7, 2026)
  105. client._month_data[key] = {'2026-07-10': _day_info()}
  106. client._month_fetched_at[key] = time.monotonic()
  107. calls = []
  108. async def _fetch(m, y):
  109. calls.append((m, y))
  110. return {}
  111. client._fetch_month_calendar = _fetch
  112. await client._get_month_calendar(7, 2026)
  113. assert calls == []
  114. async def test_expired_month_refetches_and_evicts_dayinfo_cache(self):
  115. client = _booking_client()
  116. key = (7, 2026)
  117. client._month_data[key] = {'2026-07-10': _day_info()}
  118. client._month_fetched_at[key] = time.monotonic() + _EXPIRED
  119. client._cache['2026-07-10'] = _day_info('2026-07-10')
  120. client._cache['2026-08-01'] = _day_info('2026-08-01') # different month — must survive
  121. calls = []
  122. async def _fetch(m, y):
  123. calls.append((m, y))
  124. return {'2026-07-10': _day_info()}
  125. client._fetch_month_calendar = _fetch
  126. await client._get_month_calendar(7, 2026)
  127. assert calls == [(7, 2026)]
  128. assert '2026-07-10' not in client._cache
  129. assert '2026-08-01' in client._cache
  130. assert key in client._month_fetched_at
  131. async def test_missing_timestamp_treated_as_expired(self):
  132. client = _booking_client()
  133. key = (7, 2026)
  134. client._month_data[key] = {'2026-07-10': _day_info()}
  135. calls = []
  136. async def _fetch(m, y):
  137. calls.append((m, y))
  138. return {}
  139. client._fetch_month_calendar = _fetch
  140. await client._get_month_calendar(7, 2026)
  141. assert calls == [(7, 2026)]