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