| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- """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
|