"""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.parsers import XmlParser from xsdata.formats.dataclass.serializers import XmlSerializer from xsdata.formats.dataclass.serializers.config import SerializerConfig 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 @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.""" return Reservation( id=1, customer_id=1, 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", 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.""" return Reservation( id=2, customer_id=2, 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", hotel_name="Alpine Paradise Resort", created_at=datetime.now(UTC), 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, "reservation"), ( "reservations_list should have reservation attribute" ) # 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 b"RES-2024-001" in xml_output assert b"John" in xml_output assert b"Doe" in xml_output assert b"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 b"RES-2024-001" in xml_output assert b"RES-2024-002" in xml_output assert b"John" in xml_output assert b"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 b"GuestCount" in xml_output or b"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(b'') assert b"OTA_ResRetrieveRS" in xml_output # Verify customer data is present assert b"John" in xml_output assert b"Doe" in xml_output assert b"john.doe@example.com" in xml_output # Verify reservation data is present assert b"RES-2024-001" in xml_output assert b"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_long_unique_id_truncation(self): """Test that long unique IDs are handled properly.""" customer = Customer( id=98, given_name="Test", surname="User", contact_id="CONTACT-98", ) # Unique ID at max length (35 chars) reservation = Reservation( id=98, customer_id=98, unique_id="A" * 35, # Max length 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) assert response 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 = Reservation( id=97, customer_id=97, 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_pairs = [(reservation, 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 assert b"RES-UTM-TEST" in xml_output if __name__ == "__main__": pytest.main([__file__, "-v"])