452 lines
14 KiB
Python
452 lines
14 KiB
Python
"""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 """<?xml version="1.0" encoding="UTF-8"?>
|
|
<OTA_ReadRQ xmlns="http://www.opentravel.org/OTA/2003/05"
|
|
EchoToken="12345"
|
|
TimeStamp="2024-10-07T10:00:00"
|
|
Version="8.000">
|
|
<ReadRequests>
|
|
<HotelReadRequest HotelCode="HOTEL123" HotelName="Alpine Paradise Resort">
|
|
<SelectionCriteria Start="2024-12-01" End="2025-01-31"/>
|
|
</HotelReadRequest>
|
|
</ReadRequests>
|
|
</OTA_ReadRQ>"""
|
|
|
|
|
|
@pytest.fixture
|
|
def read_request_xml_no_date_filter():
|
|
"""Sample OTA_ReadRQ XML request without date filter."""
|
|
return """<?xml version="1.0" encoding="UTF-8"?>
|
|
<OTA_ReadRQ xmlns="http://www.opentravel.org/OTA/2003/05"
|
|
EchoToken="12345"
|
|
TimeStamp="2024-10-07T10:00:00"
|
|
Version="8.000">
|
|
<ReadRequests>
|
|
<HotelReadRequest HotelCode="HOTEL123" HotelName="Alpine Paradise Resort"/>
|
|
</ReadRequests>
|
|
</OTA_ReadRQ>"""
|
|
|
|
|
|
@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'<?xml version="1.0" encoding="UTF-8"?>')
|
|
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"])
|