Created copilot instructions and testing readRequests
This commit is contained in:
@@ -1,2 +1,451 @@
|
||||
"""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"])
|
||||
|
||||
Reference in New Issue
Block a user