test_check_reservation_status.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. """Unit tests for the check_reservation_status capability (both clients) and
  2. ServiceAPI.check_reservation_status's hasattr dispatch.
  3. Deep response-parsing lives here per client; the cross-client contract
  4. (True = confirmed canceled, None = couldn't determine, never raises) is what
  5. fetch_reservations.py's guard (tests/test_reservation_guards.py) relies on.
  6. """
  7. import httpx
  8. import pytest
  9. from room_cal.service_api.client import ServiceAPI
  10. def _mock_transport(responses: list) -> httpx.MockTransport:
  11. it = iter(responses)
  12. def handler(request: httpx.Request) -> httpx.Response:
  13. item = next(it)
  14. if isinstance(item, BaseException):
  15. raise item
  16. return item
  17. return httpx.MockTransport(handler)
  18. def _airbnb_client():
  19. from room_cal.service_api._airbnb import _AirbnbClient
  20. return _AirbnbClient(cookies='test', listing_id='12345678')
  21. def _booking_client():
  22. from room_cal.service_api._booking import _BookingClient
  23. return _BookingClient(
  24. cookies='test', session='ses1', csrf_token='csrf1',
  25. hotel_id=1, account_id=1, room_id=1, rate_id='rate1',
  26. )
  27. def _stay_hosting_details_response(is_canceled: bool) -> httpx.Response:
  28. return httpx.Response(200, json={
  29. 'data': {
  30. 'presentation': {
  31. 'hostingDetails': {
  32. 'stayHostingDetails': {
  33. 'rootPlacement': [
  34. {
  35. 'sectionId': 'WHOSCOMING_SECTION',
  36. 'sectionData': {'reservationGroups': [{'isCanceled': is_canceled}]},
  37. },
  38. ],
  39. },
  40. },
  41. },
  42. },
  43. })
  44. class TestAirbnbCheckReservationStatus:
  45. async def test_canceled(self):
  46. client = _airbnb_client()
  47. client._http = httpx.AsyncClient(transport=_mock_transport([_stay_hosting_details_response(True)]))
  48. assert await client.check_reservation_status('HMXKNXCSSS') is True
  49. async def test_not_canceled(self):
  50. client = _airbnb_client()
  51. client._http = httpx.AsyncClient(transport=_mock_transport([_stay_hosting_details_response(False)]))
  52. assert await client.check_reservation_status('HMFARE3JC4') is False
  53. async def test_no_data_returns_none(self):
  54. """A confirmation code with no matching detail (e.g. typo) can't be determined."""
  55. client = _airbnb_client()
  56. client._http = httpx.AsyncClient(
  57. transport=_mock_transport([httpx.Response(200, json={'data': {'presentation': {}}})]))
  58. assert await client.check_reservation_status('BOGUS') is None
  59. async def test_timeout_returns_none_not_raise(self):
  60. client = _airbnb_client()
  61. client._http = httpx.AsyncClient(transport=_mock_transport([httpx.TimeoutException('slow')]))
  62. assert await client.check_reservation_status('HMFARE3JC4') is None
  63. async def test_reuses_details_cache(self):
  64. """A reservation already fetched (e.g. by get_booked_nets) makes no extra network call."""
  65. client = _airbnb_client()
  66. client._details.set('HMXKNXCSSS', {
  67. 'rootPlacement': [
  68. {'sectionId': 'WHOSCOMING_SECTION', 'sectionData': {'reservationGroups': [{'isCanceled': True}]}},
  69. ],
  70. })
  71. client._http = httpx.AsyncClient(transport=_mock_transport([])) # any call fails the test
  72. assert await client.check_reservation_status('HMXKNXCSSS') is True
  73. class TestBookingCheckReservationStatus:
  74. async def test_canceled(self):
  75. client = _booking_client()
  76. client._http = httpx.AsyncClient(
  77. transport=_mock_transport([httpx.Response(200, json={'success': 1, 'data': {'is_cancelled': 1}})]))
  78. assert await client.check_reservation_status('5124584185') is True
  79. async def test_not_canceled(self):
  80. client = _booking_client()
  81. client._http = httpx.AsyncClient(
  82. transport=_mock_transport([httpx.Response(200, json={'success': 1, 'data': {'is_cancelled': 0}})]))
  83. assert await client.check_reservation_status('5162718780') is False
  84. async def test_missing_field_returns_none(self):
  85. client = _booking_client()
  86. client._http = httpx.AsyncClient(
  87. transport=_mock_transport([httpx.Response(200, json={'success': 1, 'data': {}})]))
  88. assert await client.check_reservation_status('0000000000') is None
  89. async def test_transport_error_returns_none_not_raise(self):
  90. client = _booking_client()
  91. client._http = httpx.AsyncClient(transport=_mock_transport([httpx.ConnectError('down')]))
  92. assert await client.check_reservation_status('5124584185') is None
  93. async def test_no_as_token_header_sent(self):
  94. """The call relies on the post-messaging-prime session, not a page-bootstrapped token."""
  95. client = _booking_client()
  96. captured: list[httpx.Request] = []
  97. def handler(request: httpx.Request) -> httpx.Response:
  98. captured.append(request)
  99. return httpx.Response(200, json={'success': 1, 'data': {'is_cancelled': 0}})
  100. client._http = httpx.AsyncClient(transport=httpx.MockTransport(handler))
  101. await client.check_reservation_status('5162718780')
  102. assert len(captured) == 1
  103. assert 'x-booking-as-token' not in {h.lower() for h in captured[0].headers.keys()}
  104. assert 'hotel_account_id' not in captured[0].url.params
  105. class TestServiceAPIDispatch:
  106. async def test_unknown_service_returns_none(self):
  107. api = ServiceAPI(clients={})
  108. assert await api.check_reservation_status('airbnb', 'ANY') is None
  109. async def test_client_without_capability_returns_none(self):
  110. class _NoCapabilityClient:
  111. pass
  112. api = ServiceAPI(clients={'airbnb': _NoCapabilityClient()})
  113. assert await api.check_reservation_status('airbnb', 'ANY') is None
  114. async def test_dispatches_to_client_method(self):
  115. class _StubClient:
  116. async def check_reservation_status(self, reservation_id):
  117. return reservation_id == 'CANCELED-ID'
  118. api = ServiceAPI(clients={'airbnb': _StubClient()})
  119. assert await api.check_reservation_status('airbnb', 'CANCELED-ID') is True
  120. assert await api.check_reservation_status('airbnb', 'OTHER-ID') is False