"""Tests for AlpineBits server read action. This module tests the ReadAction handler which retrieves reservations from the database and returns them as OTA_ResRetrieveRS XML. """ from datetime import UTC, date, datetime import pytest from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from xsdata.formats.dataclass.serializers.config import SerializerConfig from xsdata_pydantic.bindings import XmlParser, XmlSerializer from alpine_bits_python.alpine_bits_helpers import create_res_retrieve_response from alpine_bits_python.alpinebits_server import AlpineBitsClientInfo from alpine_bits_python.db import Base, Customer, Reservation from alpine_bits_python.generated import OtaReadRq from alpine_bits_python.generated.alpinebits import OtaResRetrieveRs from alpine_bits_python.schemas import ReservationData @pytest.fixture async def test_db_engine(): """Create an in-memory SQLite database for testing.""" engine = create_async_engine( "sqlite+aiosqlite:///:memory:", echo=False, ) # Create tables async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield engine # Cleanup await engine.dispose() @pytest.fixture async def test_db_session(test_db_engine): """Create a test database session.""" async_session = async_sessionmaker( test_db_engine, class_=AsyncSession, expire_on_commit=False, ) async with async_session() as session: yield session @pytest.fixture def sample_customer(): """Create a sample customer for testing.""" return Customer( id=1, given_name="John", surname="Doe", contact_id="CONTACT-12345", name_prefix="Mr.", name_title="Jr.", email_address="john.doe@example.com", phone="+1234567890", email_newsletter=True, address_line="123 Main Street", city_name="Anytown", postal_code="12345", country_code="US", gender="Male", birth_date="1980-01-01", language="en", address_catalog=False, ) @pytest.fixture def sample_reservation(sample_customer): """Create a sample reservation for testing.""" reservation = ReservationData( unique_id="RES-2024-001", start_date=date(2024, 12, 25), end_date=date(2024, 12, 31), num_adults=2, num_children=1, children_ages=[8], offer="Christmas Special", created_at=datetime.now(UTC), utm_source="google", utm_medium="cpc", utm_campaign="winter2024", utm_term="ski resort", utm_content="ad1", user_comment="Late check-in requested", fbclid="", gclid="abc123xyz", hotel_code="HOTEL123", hotel_name="Alpine Paradise Resort", ) data = reservation.model_dump(exclude_none=True) children_list = data.pop("children_ages", []) children_csv = ",".join(str(int(a)) for a in children_list) if children_list else "" data["children_ages"] = children_csv print(data) return Reservation( id=1, customer_id=1, **data, customer=sample_customer, ) @pytest.fixture def minimal_customer(): """Create a minimal customer with only required fields.""" return Customer( id=2, given_name="Jane", surname="Smith", contact_id="CONTACT-67890", ) @pytest.fixture def minimal_reservation(minimal_customer): """Create a minimal reservation with only required fields.""" reservation = ReservationData( unique_id="RES-2024-002", start_date=date(2025, 1, 15), end_date=date(2025, 1, 20), num_adults=1, num_children=0, children_ages=[], hotel_code="HOTEL123", created_at=datetime.now(UTC), hotel_name="Alpine Paradise Resort", ) data = reservation.model_dump(exclude_none=True) children_list = data.pop("children_ages", []) children_csv = ",".join(str(int(a)) for a in children_list) if children_list else "" data["children_ages"] = children_csv return Reservation( id=2, customer_id=2, **data, customer=minimal_customer, ) @pytest.fixture def read_request_xml(): """Sample OTA_ReadRQ XML request.""" return """ """ @pytest.fixture def read_request_xml_no_date_filter(): """Sample OTA_ReadRQ XML request without date filter.""" return """ """ @pytest.fixture def test_config(): """Test configuration with hotel credentials.""" return { "hotels": [ { "hotel_id": "HOTEL123", "username": "testuser", "password": "testpass", } ] } @pytest.fixture def client_info(): """Sample client info for testing.""" return AlpineBitsClientInfo( username="testuser", password="testpass", client_id="CLIENT-001", ) class TestCreateResRetrieveResponse: """Test the create_res_retrieve_response function.""" def test_empty_list(self): """Test creating response with empty reservation list.""" response = create_res_retrieve_response([]) assert response is not None, "Response should not be None" # check that response is of correct type assert isinstance(response, OtaResRetrieveRs), ( "Response should be of type OtaResRetrieveRs" ) assert hasattr(response, "success"), "Response should have success attribute" assert hasattr(response, "reservations_list"), ( "Response should have reservations_list attribute" ) def test_single_reservation(self, sample_reservation, sample_customer): """Test creating response with single reservation.""" reservation_pairs = [(sample_reservation, sample_customer)] response = create_res_retrieve_response(reservation_pairs) assert response is not None assert hasattr(response, "reservations_list"), ( "Response should have reservations_list attribute" ) assert hasattr(response.reservations_list, "hotel_reservation"), ( "reservations_list should have reservation attribute" ) assert len(response.reservations_list.hotel_reservation) == 1 res: OtaResRetrieveRs.ReservationsList.HotelReservation = ( response.reservations_list.hotel_reservation[0] ) assert res.unique_id is not None, "Reservation should have unique_id" # Verify the response can be serialized to XML config = SerializerConfig( pretty_print=True, xml_declaration=True, encoding="UTF-8" ) serializer = XmlSerializer(config=config) xml_output = serializer.render( response, ns_map={None: "http://www.opentravel.org/OTA/2003/05"} ) assert xml_output is not None # assert "RES-2024-001" in xml_output does not work due to hashing assert "John" in xml_output assert "Doe" in xml_output assert "HOTEL123" in xml_output def test_multiple_reservations( self, sample_reservation, sample_customer, minimal_reservation, minimal_customer, ): """Test creating response with multiple reservations.""" reservation_pairs = [ (sample_reservation, sample_customer), (minimal_reservation, minimal_customer), ] response = create_res_retrieve_response(reservation_pairs) assert response is not None # Serialize to XML and verify both reservations are present config = SerializerConfig( pretty_print=True, xml_declaration=True, encoding="UTF-8" ) serializer = XmlSerializer(config=config) xml_output = serializer.render( response, ns_map={None: "http://www.opentravel.org/OTA/2003/05"} ) # assert "RES-2024-001" in xml_output # assert "RES-2024-002" in xml_output assert "John" in xml_output assert "Jane" in xml_output def test_reservation_with_children(self, sample_reservation, sample_customer): """Test reservation with children ages.""" sample_reservation.num_children = 2 sample_reservation.children_ages = "8,5" reservation_pairs = [(sample_reservation, sample_customer)] response = create_res_retrieve_response(reservation_pairs) config = SerializerConfig(pretty_print=True) serializer = XmlSerializer(config=config) xml_output = serializer.render( response, ns_map={None: "http://www.opentravel.org/OTA/2003/05"} ) assert response is not None # Children should be represented in guest counts assert "GuestCount" in xml_output or "Child" in xml_output class TestXMLParsing: """Test XML parsing and generation.""" def test_parse_read_request(self, read_request_xml): """Test parsing of OTA_ReadRQ XML.""" parser = XmlParser() read_request = parser.from_string(read_request_xml, OtaReadRq) assert read_request is not None assert read_request.read_requests is not None assert read_request.read_requests.hotel_read_request is not None hotel_req = read_request.read_requests.hotel_read_request assert hotel_req.hotel_code == "HOTEL123" assert hotel_req.hotel_name == "Alpine Paradise Resort" assert hotel_req.selection_criteria is not None assert hotel_req.selection_criteria.start == "2024-12-01" def test_parse_read_request_no_date(self, read_request_xml_no_date_filter): """Test parsing of OTA_ReadRQ without date filter.""" parser = XmlParser() read_request = parser.from_string(read_request_xml_no_date_filter, OtaReadRq) assert read_request is not None hotel_req = read_request.read_requests.hotel_read_request assert hotel_req.hotel_code == "HOTEL123" assert hotel_req.selection_criteria is None def test_serialize_retrieve_response( self, sample_reservation, sample_customer, ): """Test serialization of retrieve response to XML.""" reservation_pairs = [(sample_reservation, sample_customer)] response = create_res_retrieve_response(reservation_pairs) config = SerializerConfig( pretty_print=True, xml_declaration=True, encoding="UTF-8" ) serializer = XmlSerializer(config=config) xml_output = serializer.render( response, ns_map={None: "http://www.opentravel.org/OTA/2003/05"} ) # Verify it's valid XML assert xml_output.startswith('') assert "OTA_ResRetrieveRS" in xml_output # Verify customer data is present assert "John" in xml_output assert "Doe" in xml_output assert "john.doe@example.com" in xml_output # Verify reservation data is present # assert "RES-2024-001" in xml_output assert "HOTEL123" in xml_output class TestEdgeCases: """Test edge cases and error conditions.""" def test_customer_with_special_characters(self): """Test customer with special characters in name.""" customer = Customer( id=99, given_name="François", surname="O'Brien-Smith", contact_id="CONTACT-SPECIAL", ) reservation = Reservation( id=99, customer_id=99, unique_id="RES-SPECIAL", start_date=date(2025, 1, 1), end_date=date(2025, 1, 5), num_adults=1, num_children=0, children_ages="", hotel_code="HOTEL123", created_at=datetime.now(UTC), ) reservation_pairs = [(reservation, customer)] response = create_res_retrieve_response(reservation_pairs) config = SerializerConfig(pretty_print=True, encoding="UTF-8") serializer = XmlSerializer(config=config) xml_output = serializer.render( response, ns_map={None: "http://www.opentravel.org/OTA/2003/05"} ) assert response is not None assert xml_output is not None def test_reservation_with_all_utm_parameters(self): """Test reservation with all UTM tracking parameters.""" customer = Customer( id=97, given_name="Marketing", surname="Test", contact_id="CONTACT-97", ) reservation = ReservationData( unique_id="RES-UTM-TEST", start_date=date(2025, 2, 1), end_date=date(2025, 2, 7), num_adults=2, num_children=0, children_ages=[], hotel_code="HOTEL123", created_at=datetime.now(UTC), utm_source="facebook", utm_medium="social", utm_campaign="spring2025", utm_term="luxury resort", utm_content="carousel_ad", fbclid="IwAR1234567890", gclid="", ) reservation_db = Reservation( id=97, customer_id=97, **reservation.model_dump(exclude_none=True), ) reservation_pairs = [(reservation_db, customer)] response = create_res_retrieve_response(reservation_pairs) config = SerializerConfig(pretty_print=True) serializer = XmlSerializer(config=config) xml_output = serializer.render( response, ns_map={None: "http://www.opentravel.org/OTA/2003/05"} ) assert response is not None # UTM parameters should be in comments or other fields if __name__ == "__main__": pytest.main([__file__, "-v"])