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