diff --git a/tests/test_alpine_bits_server_read.py b/tests/test_alpine_bits_server_read.py index 67cf0db..ae3f2a5 100644 --- a/tests/test_alpine_bits_server_read.py +++ b/tests/test_alpine_bits_server_read.py @@ -4,22 +4,28 @@ This module tests the ReadAction handler which retrieves reservations from the database and returns them as OTA_ResRetrieveRS XML. """ +import hashlib from datetime import UTC, date, datetime import pytest +import pytest_asyncio +from sqlalchemy import select 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.alpinebits_server import AlpineBitsClientInfo, AlpineBitsServer +from alpine_bits_python.db import AckedRequest, 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 +# HTTP status code constants +HTTP_OK = 200 -@pytest.fixture + +@pytest_asyncio.fixture async def test_db_engine(): """Create an in-memory SQLite database for testing.""" engine = create_async_engine( @@ -37,7 +43,7 @@ async def test_db_engine(): await engine.dispose() -@pytest.fixture +@pytest_asyncio.fixture async def test_db_session(test_db_engine): """Create a test database session.""" async_session = async_sessionmaker( @@ -187,9 +193,10 @@ def read_request_xml_no_date_filter(): def test_config(): """Test configuration with hotel credentials.""" return { - "hotels": [ + "alpine_bits_auth": [ { "hotel_id": "HOTEL123", + "hotel_name": "Alpine Paradise Resort", "username": "testuser", "password": "testpass", } @@ -451,5 +458,334 @@ class TestEdgeCases: # UTM parameters should be in comments or other fields +class TestAcknowledgments: + """Test acknowledgments. + + 1. Setup AlpineBitsServer so that it can respond to sample read requests. + 2. Send acknowledgment requests and verify responses. + 3. Verify that acknowledgments are recorded in the database. + 4. Verify that Read Requests no longer return already acknowledged reservations. + 5. Verify that that still happens when SelectionCriteria date filters are applied. + + """ + + @pytest_asyncio.fixture + async def populated_db_session( + self, + test_db_session, + sample_reservation, + sample_customer, + minimal_reservation, + minimal_customer, + ): + """Create a database session with sample data.""" + # Add customers + test_db_session.add(sample_customer) + test_db_session.add(minimal_customer) + await test_db_session.commit() + + # Add reservations + test_db_session.add(sample_reservation) + test_db_session.add(minimal_reservation) + await test_db_session.commit() + + return test_db_session + + @pytest.fixture + def alpinebits_server(self, test_config): + """Create AlpineBitsServer instance for testing.""" + return AlpineBitsServer(config=test_config) + + @pytest.fixture + def notif_report_xml_template(self): + """Template for OTA_NotifReportRQ XML request.""" + return """ + + + + + {reservations} + + + +""" + + def create_notif_report_xml(self, unique_ids): + """Create a notification report XML with given unique IDs.""" + template = """ + + + + + {reservations} + + + +""" + + reservations = "" + for unique_id in unique_ids: + reservations += f'' + + return template.format(reservations=reservations) + + @pytest.mark.asyncio + async def test_setup_server_responds_to_read_requests( + self, alpinebits_server, populated_db_session, client_info, read_request_xml + ): + """Test 1: Setup AlpineBitsServer so that it can respond to sample read requests.""" + # Send a read request and verify we get a response + response = await alpinebits_server.handle_request( + request_action_name="OTA_Read:GuestRequests", + request_xml=read_request_xml, + client_info=client_info, + version="2024-10", + dbsession=populated_db_session, + ) + + assert response is not None + assert response.status_code == HTTP_OK + assert response.xml_content is not None + + # Verify response contains reservation data + assert "OTA_ResRetrieveRS" in response.xml_content + assert "HOTEL123" in response.xml_content + + @pytest.mark.asyncio + async def test_send_acknowledgment_and_verify_response( + self, alpinebits_server, populated_db_session, client_info + ): + """Test 2: Send acknowledgment requests and verify responses.""" + # First, get the unique IDs from a read request + read_xml = """ + + + + +""" + + # Get reservations first + _read_response = await alpinebits_server.handle_request( + request_action_name="OTA_Read:GuestRequests", + request_xml=read_xml, + client_info=client_info, + version="2024-10", + dbsession=populated_db_session, + ) + + # Extract unique IDs from the response (we'll use test unique IDs) + test_unique_ids = [ + "RES-2024-001", + "RES-2024-002", + ] # In reality, these would be extracted from read response + + # Create acknowledgment request + notif_report_xml = self.create_notif_report_xml(test_unique_ids) + + # Send acknowledgment + ack_response = await alpinebits_server.handle_request( + request_action_name="OTA_NotifReport:GuestRequests", + request_xml=notif_report_xml, + client_info=client_info, + version="2024-10", + dbsession=populated_db_session, + ) + + assert ack_response is not None + assert ack_response.status_code == HTTP_OK + assert "OTA_NotifReportRS" in ack_response.xml_content + + @pytest.mark.asyncio + async def test_acknowledgments_recorded_in_database( + self, alpinebits_server, populated_db_session, client_info + ): + """Test 3: Verify that acknowledgments are recorded in the database.""" + # Create acknowledgment request + test_unique_ids = ["test-ack-id-1", "test-ack-id-2"] + notif_report_xml = self.create_notif_report_xml(test_unique_ids) + + # Count existing acked requests + result = await populated_db_session.execute(select(AckedRequest)) + initial_count = len(result.all()) + + # Send acknowledgment + await alpinebits_server.handle_request( + request_action_name="OTA_NotifReport:GuestRequests", + request_xml=notif_report_xml, + client_info=client_info, + version="2024-10", + dbsession=populated_db_session, + ) + + # Verify acknowledgments were recorded + result = await populated_db_session.execute(select(AckedRequest)) + acked_requests = result.all() + assert len(acked_requests) == initial_count + 2 + + # Verify the specific acknowledgments + acked_ids = [req[0].unique_id for req in acked_requests] + assert "test-ack-id-1" in acked_ids + assert "test-ack-id-2" in acked_ids + + # Verify client ID is recorded + for req in acked_requests[-2:]: # Last 2 requests + assert req[0].client_id == client_info.client_id + + @pytest.mark.asyncio + async def test_read_excludes_acknowledged_reservations( + self, alpinebits_server, populated_db_session, client_info + ): + """Test 4: Verify that Read Requests no longer return already acknowledged reservations.""" + # First read request - should return all reservations + read_xml = """ + + + + +""" + + initial_response = await alpinebits_server.handle_request( + request_action_name="OTA_Read:GuestRequests", + request_xml=read_xml, + client_info=client_info, + version="2024-10", + dbsession=populated_db_session, + ) + + # Parse response to count initial reservations + parser = XmlParser() + initial_parsed = parser.from_string( + initial_response.xml_content, OtaResRetrieveRs + ) + initial_count = 0 + if ( + initial_parsed.reservations_list + and initial_parsed.reservations_list.hotel_reservation + ): + initial_count = len(initial_parsed.reservations_list.hotel_reservation) + + # Acknowledge one reservation by using its MD5 hash + # Get the unique_id from sample reservation and create its MD5 + sample_unique_id = "RES-2024-001" + md5_hash = hashlib.md5(sample_unique_id.encode()).hexdigest() + + # Manually insert acknowledgment + acked_request = AckedRequest( + unique_id=md5_hash, + client_id=client_info.client_id, + timestamp=datetime.now(UTC), + ) + populated_db_session.add(acked_request) + await populated_db_session.commit() + + # Second read request - should return fewer reservations + second_response = await alpinebits_server.handle_request( + request_action_name="OTA_Read:GuestRequests", + request_xml=read_xml, + client_info=client_info, + version="2024-10", + dbsession=populated_db_session, + ) + + # Parse second response + second_parsed = parser.from_string( + second_response.xml_content, OtaResRetrieveRs + ) + second_count = 0 + if ( + second_parsed.reservations_list + and second_parsed.reservations_list.hotel_reservation + ): + second_count = len(second_parsed.reservations_list.hotel_reservation) + + # Should have one fewer reservation + assert second_count == initial_count - 1 + + @pytest.mark.asyncio + async def test_acknowledgments_work_with_date_filters( + self, alpinebits_server, populated_db_session, client_info + ): + """Test 5: Verify acknowledgments still work when SelectionCriteria date filters are applied.""" + # Read request with date filter + read_xml_with_date = """ + + + + + + +""" + + # First read with date filter + initial_response = await alpinebits_server.handle_request( + request_action_name="OTA_Read:GuestRequests", + request_xml=read_xml_with_date, + client_info=client_info, + version="2024-10", + dbsession=populated_db_session, + ) + + parser = XmlParser() + initial_parsed = parser.from_string( + initial_response.xml_content, OtaResRetrieveRs + ) + initial_count = 0 + if ( + initial_parsed.reservations_list + and initial_parsed.reservations_list.hotel_reservation + ): + initial_count = len(initial_parsed.reservations_list.hotel_reservation) + + # Acknowledge one reservation that falls within the date range + # The sample_reservation has dates 2024-12-25 to 2024-12-31, which should be in range + sample_unique_id = "RES-2024-001" + md5_hash = hashlib.md5(sample_unique_id.encode()).hexdigest() + + acked_request = AckedRequest( + unique_id=md5_hash, + client_id=client_info.client_id, + timestamp=datetime.now(UTC), + ) + populated_db_session.add(acked_request) + await populated_db_session.commit() + + # Second read with same date filter + second_response = await alpinebits_server.handle_request( + request_action_name="OTA_Read:GuestRequests", + request_xml=read_xml_with_date, + client_info=client_info, + version="2024-10", + dbsession=populated_db_session, + ) + + second_parsed = parser.from_string( + second_response.xml_content, OtaResRetrieveRs + ) + second_count = 0 + if ( + second_parsed.reservations_list + and second_parsed.reservations_list.hotel_reservation + ): + second_count = len(second_parsed.reservations_list.hotel_reservation) + + # Should have fewer reservations even with date filter + assert second_count < initial_count + + if __name__ == "__main__": pytest.main([__file__, "-v"])