|
|
@@ -0,0 +1,150 @@
|
|
|
+"""Unit tests for the check_reservation_status capability (both clients) and
|
|
|
+ServiceAPI.check_reservation_status's hasattr dispatch.
|
|
|
+
|
|
|
+Deep response-parsing lives here per client; the cross-client contract
|
|
|
+(True = confirmed canceled, None = couldn't determine, never raises) is what
|
|
|
+fetch_reservations.py's guard (tests/test_reservation_guards.py) relies on.
|
|
|
+"""
|
|
|
+import httpx
|
|
|
+import pytest
|
|
|
+
|
|
|
+from room_cal.service_api.client import ServiceAPI
|
|
|
+
|
|
|
+
|
|
|
+def _mock_transport(responses: list) -> httpx.MockTransport:
|
|
|
+ it = iter(responses)
|
|
|
+
|
|
|
+ def handler(request: httpx.Request) -> httpx.Response:
|
|
|
+ item = next(it)
|
|
|
+ if isinstance(item, BaseException):
|
|
|
+ raise item
|
|
|
+ return item
|
|
|
+
|
|
|
+ return httpx.MockTransport(handler)
|
|
|
+
|
|
|
+
|
|
|
+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='ses1', csrf_token='csrf1',
|
|
|
+ hotel_id=1, account_id=1, room_id=1, rate_id='rate1',
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _stay_hosting_details_response(is_canceled: bool) -> httpx.Response:
|
|
|
+ return httpx.Response(200, json={
|
|
|
+ 'data': {
|
|
|
+ 'presentation': {
|
|
|
+ 'hostingDetails': {
|
|
|
+ 'stayHostingDetails': {
|
|
|
+ 'rootPlacement': [
|
|
|
+ {
|
|
|
+ 'sectionId': 'WHOSCOMING_SECTION',
|
|
|
+ 'sectionData': {'reservationGroups': [{'isCanceled': is_canceled}]},
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+
|
|
|
+class TestAirbnbCheckReservationStatus:
|
|
|
+ async def test_canceled(self):
|
|
|
+ client = _airbnb_client()
|
|
|
+ client._http = httpx.AsyncClient(transport=_mock_transport([_stay_hosting_details_response(True)]))
|
|
|
+ assert await client.check_reservation_status('HMXKNXCSSS') is True
|
|
|
+
|
|
|
+ async def test_not_canceled(self):
|
|
|
+ client = _airbnb_client()
|
|
|
+ client._http = httpx.AsyncClient(transport=_mock_transport([_stay_hosting_details_response(False)]))
|
|
|
+ assert await client.check_reservation_status('HMFARE3JC4') is False
|
|
|
+
|
|
|
+ async def test_no_data_returns_none(self):
|
|
|
+ """A confirmation code with no matching detail (e.g. typo) can't be determined."""
|
|
|
+ client = _airbnb_client()
|
|
|
+ client._http = httpx.AsyncClient(
|
|
|
+ transport=_mock_transport([httpx.Response(200, json={'data': {'presentation': {}}})]))
|
|
|
+ assert await client.check_reservation_status('BOGUS') is None
|
|
|
+
|
|
|
+ async def test_timeout_returns_none_not_raise(self):
|
|
|
+ client = _airbnb_client()
|
|
|
+ client._http = httpx.AsyncClient(transport=_mock_transport([httpx.TimeoutException('slow')]))
|
|
|
+ assert await client.check_reservation_status('HMFARE3JC4') is None
|
|
|
+
|
|
|
+ async def test_reuses_details_cache(self):
|
|
|
+ """A reservation already fetched (e.g. by get_booked_nets) makes no extra network call."""
|
|
|
+ client = _airbnb_client()
|
|
|
+ client._details.set('HMXKNXCSSS', {
|
|
|
+ 'rootPlacement': [
|
|
|
+ {'sectionId': 'WHOSCOMING_SECTION', 'sectionData': {'reservationGroups': [{'isCanceled': True}]}},
|
|
|
+ ],
|
|
|
+ })
|
|
|
+ client._http = httpx.AsyncClient(transport=_mock_transport([])) # any call fails the test
|
|
|
+ assert await client.check_reservation_status('HMXKNXCSSS') is True
|
|
|
+
|
|
|
+
|
|
|
+class TestBookingCheckReservationStatus:
|
|
|
+ async def test_canceled(self):
|
|
|
+ client = _booking_client()
|
|
|
+ client._http = httpx.AsyncClient(
|
|
|
+ transport=_mock_transport([httpx.Response(200, json={'success': 1, 'data': {'is_cancelled': 1}})]))
|
|
|
+ assert await client.check_reservation_status('5124584185') is True
|
|
|
+
|
|
|
+ async def test_not_canceled(self):
|
|
|
+ client = _booking_client()
|
|
|
+ client._http = httpx.AsyncClient(
|
|
|
+ transport=_mock_transport([httpx.Response(200, json={'success': 1, 'data': {'is_cancelled': 0}})]))
|
|
|
+ assert await client.check_reservation_status('5162718780') is False
|
|
|
+
|
|
|
+ async def test_missing_field_returns_none(self):
|
|
|
+ client = _booking_client()
|
|
|
+ client._http = httpx.AsyncClient(
|
|
|
+ transport=_mock_transport([httpx.Response(200, json={'success': 1, 'data': {}})]))
|
|
|
+ assert await client.check_reservation_status('0000000000') is None
|
|
|
+
|
|
|
+ async def test_transport_error_returns_none_not_raise(self):
|
|
|
+ client = _booking_client()
|
|
|
+ client._http = httpx.AsyncClient(transport=_mock_transport([httpx.ConnectError('down')]))
|
|
|
+ assert await client.check_reservation_status('5124584185') is None
|
|
|
+
|
|
|
+ async def test_no_as_token_header_sent(self):
|
|
|
+ """The call relies on the post-messaging-prime session, not a page-bootstrapped token."""
|
|
|
+ client = _booking_client()
|
|
|
+ captured: list[httpx.Request] = []
|
|
|
+
|
|
|
+ def handler(request: httpx.Request) -> httpx.Response:
|
|
|
+ captured.append(request)
|
|
|
+ return httpx.Response(200, json={'success': 1, 'data': {'is_cancelled': 0}})
|
|
|
+
|
|
|
+ client._http = httpx.AsyncClient(transport=httpx.MockTransport(handler))
|
|
|
+ await client.check_reservation_status('5162718780')
|
|
|
+ assert len(captured) == 1
|
|
|
+ assert 'x-booking-as-token' not in {h.lower() for h in captured[0].headers.keys()}
|
|
|
+ assert 'hotel_account_id' not in captured[0].url.params
|
|
|
+
|
|
|
+
|
|
|
+class TestServiceAPIDispatch:
|
|
|
+ async def test_unknown_service_returns_none(self):
|
|
|
+ api = ServiceAPI(clients={})
|
|
|
+ assert await api.check_reservation_status('airbnb', 'ANY') is None
|
|
|
+
|
|
|
+ async def test_client_without_capability_returns_none(self):
|
|
|
+ class _NoCapabilityClient:
|
|
|
+ pass
|
|
|
+ api = ServiceAPI(clients={'airbnb': _NoCapabilityClient()})
|
|
|
+ assert await api.check_reservation_status('airbnb', 'ANY') is None
|
|
|
+
|
|
|
+ async def test_dispatches_to_client_method(self):
|
|
|
+ class _StubClient:
|
|
|
+ async def check_reservation_status(self, reservation_id):
|
|
|
+ return reservation_id == 'CANCELED-ID'
|
|
|
+ api = ServiceAPI(clients={'airbnb': _StubClient()})
|
|
|
+ assert await api.check_reservation_status('airbnb', 'CANCELED-ID') is True
|
|
|
+ assert await api.check_reservation_status('airbnb', 'OTHER-ID') is False
|