Added some tests for Handshakes

This commit is contained in:
Jonas Linter
2025-10-02 14:26:06 +02:00
parent 233a682e35
commit 82118a1fa8
8 changed files with 313 additions and 70 deletions

View File

@@ -17,6 +17,7 @@ dependencies = [
"httpx>=0.28.1", "httpx>=0.28.1",
"lxml>=6.0.1", "lxml>=6.0.1",
"pytest>=8.4.2", "pytest>=8.4.2",
"pytest-asyncio>=1.2.0",
"redis>=6.4.0", "redis>=6.4.0",
"ruff>=0.13.1", "ruff>=0.13.1",
"slowapi>=0.1.9", "slowapi>=0.1.9",

View File

@@ -3,10 +3,9 @@ from typing import Union
import sys import sys
import os import os
# Add the src directory to the path so we can import our modules
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from simplified_access import (
from alpine_bits_python.alpine_bits_helpers import (
CustomerData, CustomerData,
CustomerFactory, CustomerFactory,
ResGuestFactory, ResGuestFactory,

View File

View File

@@ -0,0 +1,51 @@
import pytest
import asyncio
from alpine_bits_python.alpinebits_server import AlpineBitsServer, AlpineBitsClientInfo
@pytest.mark.asyncio
async def test_ping_action_response_success():
server = AlpineBitsServer()
with open("test/test_data/Handshake-OTA_PingRQ.xml", "r", encoding="utf-8") as f:
request_xml = f.read()
client_info = AlpineBitsClientInfo(username="irrelevant", password="irrelevant")
response = await server.handle_request(
request_action_name="OTA_Ping:Handshaking",
request_xml=request_xml,
client_info=client_info,
version="2024-10"
)
assert response.status_code == 200
assert "<OTA_PingRS" in response.xml_content
assert "<Success" in response.xml_content
assert "Version=" in response.xml_content
@pytest.mark.asyncio
async def test_ping_action_response_version_arbitrary():
server = AlpineBitsServer()
with open("test/test_data/Handshake-OTA_PingRQ.xml", "r", encoding="utf-8") as f:
request_xml = f.read()
client_info = AlpineBitsClientInfo(username="irrelevant", password="irrelevant")
response = await server.handle_request(
request_action_name="OTA_Ping:Handshaking",
request_xml=request_xml,
client_info=client_info,
version="2022-10"
)
assert response.status_code == 200
assert "<OTA_PingRS" in response.xml_content
assert "Version=" in response.xml_content
@pytest.mark.asyncio
async def test_ping_action_response_invalid_action():
server = AlpineBitsServer()
with open("test/test_data/Handshake-OTA_PingRQ.xml", "r", encoding="utf-8") as f:
request_xml = f.read()
client_info = AlpineBitsClientInfo(username="irrelevant", password="irrelevant")
response = await server.handle_request(
request_action_name="InvalidAction",
request_xml=request_xml,
client_info=client_info,
version="2024-10"
)
assert response.status_code == 400
assert "Error" in response.xml_content

View File

@@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
AlpineBits 2024-10
https://www.alpinebits.org/
Sample message file for a Handshake request
Changelog:
v. 2024-10 1.2 Example extended with all capabilities and two supported releases
v. 2024-10 1.1 Removed the OTA_Ping action
v. 2024-10 1.0 added supported version 2024-10 in the example
v. 2018-10 1.0 initial example
-->
<OTA_PingRQ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.opentravel.org/OTA/2003/05"
xsi:schemaLocation="http://www.opentravel.org/OTA/2003/05 OTA_PingRQ.xsd"
Version="8.000">
<EchoData>
{
"versions": [
{
"version": "2024-10",
"actions": [
{
"action": "action_OTA_Read"
},
{
"action": "action_OTA_HotelResNotif_GuestRequests"
},
{
"action": "action_OTA_HotelResNotif_GuestRequests_StatusUpdate"
},
{
"action": "action_OTA_HotelInvCountNotif",
"supports": [
"OTA_HotelInvCountNotif_accept_rooms",
"OTA_HotelInvCountNotif_accept_categories",
"OTA_HotelInvCountNotif_accept_deltas",
"OTA_HotelInvCountNotif_accept_out_of_market",
"OTA_HotelInvCountNotif_accept_out_of_order",
"OTA_HotelInvCountNotif_accept_complete_set",
"OTA_HotelInvCountNotif_accept_closing_seasons"
]
},
{
"action": "action_OTA_HotelDescriptiveContentNotif_Inventory",
"supports": [
"OTA_HotelDescriptiveContentNotif_Inventory_use_rooms",
"OTA_HotelDescriptiveContentNotif_Inventory_occupancy_children"
]
},
{
"action": "action_OTA_HotelDescriptiveContentNotif_Info"
},
{
"action": "action_OTA_HotelDescriptiveInfo_Inventory"
},
{
"action": "action_OTA_HotelDescriptiveInfo_Info"
},
{
"action": "action_OTA_HotelRatePlanNotif_RatePlans",
"supports": [
"OTA_HotelRatePlanNotif_accept_ArrivalDOW",
"OTA_HotelRatePlanNotif_accept_DepartureDOW",
"OTA_HotelRatePlanNotif_accept_RatePlan_BookingRule",
"OTA_HotelRatePlanNotif_accept_RatePlan_RoomType_BookingRule",
"OTA_HotelRatePlanNotif_accept_RatePlan_mixed_BookingRule",
"OTA_HotelRatePlanNotif_accept_Supplements",
"OTA_HotelRatePlanNotif_accept_FreeNightsOffers",
"OTA_HotelRatePlanNotif_accept_FamilyOffers",
"OTA_HotelRatePlanNotif_accept_full",
"OTA_HotelRatePlanNotif_accept_overlay",
"OTA_HotelRatePlanNotif_accept_RatePlanJoin",
"OTA_HotelRatePlanNotif_accept_OfferRule_BookingOffset",
"OTA_HotelRatePlanNotif_accept_OfferRule_DOWLOS"
]
},
{
"action": "action_OTA_HotelRatePlan_BaseRates",
"supports": [
"OTA_HotelRatePlan_BaseRates_deltas"
]
},
{
"action": "action_OTA_HotelPostEventNotif_EventReports"
}
]
},
{
"version": "2022-10",
"actions": [
{
"action": "action_OTA_Ping"
},
{
"action": "action_OTA_Read"
},
{
"action": "action_OTA_HotelResNotif_GuestRequests"
},
{
"action": "action_OTA_HotelResNotif_GuestRequests_StatusUpdate"
},
{
"action": "action_OTA_HotelInvCountNotif",
"supports": [
"OTA_HotelInvCountNotif_accept_rooms",
"OTA_HotelInvCountNotif_accept_categories",
"OTA_HotelInvCountNotif_accept_deltas",
"OTA_HotelInvCountNotif_accept_out_of_market",
"OTA_HotelInvCountNotif_accept_out_of_order",
"OTA_HotelInvCountNotif_accept_complete_set",
"OTA_HotelInvCountNotif_accept_closing_seasons"
]
},
{
"action": "action_OTA_HotelDescriptiveContentNotif_Inventory",
"supports": [
"OTA_HotelDescriptiveContentNotif_Inventory_use_rooms",
"OTA_HotelDescriptiveContentNotif_Inventory_occupancy_children"
]
},
{
"action": "action_OTA_HotelDescriptiveContentNotif_Info"
},
{
"action": "action_OTA_HotelDescriptiveInfo_Inventory"
},
{
"action": "action_OTA_HotelDescriptiveInfo_Info"
},
{
"action": "action_OTA_HotelRatePlanNotif_RatePlans",
"supports": [
"OTA_HotelRatePlanNotif_accept_ArrivalDOW",
"OTA_HotelRatePlanNotif_accept_DepartureDOW",
"OTA_HotelRatePlanNotif_accept_RatePlan_BookingRule",
"OTA_HotelRatePlanNotif_accept_RatePlan_RoomType_BookingRule",
"OTA_HotelRatePlanNotif_accept_RatePlan_mixed_BookingRule",
"OTA_HotelRatePlanNotif_accept_Supplements",
"OTA_HotelRatePlanNotif_accept_FreeNightsOffers",
"OTA_HotelRatePlanNotif_accept_FamilyOffers",
"OTA_HotelRatePlanNotif_accept_overlay",
"OTA_HotelRatePlanNotif_accept_RatePlanJoin",
"OTA_HotelRatePlanNotif_accept_OfferRule_BookingOffset",
"OTA_HotelRatePlanNotif_accept_OfferRule_DOWLOS"
]
}
]
}
]
}
</EchoData>
</OTA_PingRQ>

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
AlpineBits 2024-10
https://www.alpinebits.org/
Sample message file for a Handshake response
Changelog:
v. 2024-10 1.2 Example extended with all capabilities and two supported releases
v. 2024-10 1.1 Removed the OTA_Ping action
v. 2024-10 1.0 added supported version 2024-10 in the example
v. 2018-10 1.0 initial example
-->
<OTA_PingRS xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.opentravel.org/OTA/2003/05"
xsi:schemaLocation="http://www.opentravel.org/OTA/2003/05 OTA_PingRS.xsd"
Version="8.000">
<Success/>
<Warnings>
<Warning Type="11" Status="ALPINEBITS_HANDSHAKE">{
"versions": [
{
"version": "2024-10",
"actions": [
{
"action": "action_OTA_Read"
},
{
"action": "action_OTA_HotelResNotif_GuestRequests"
}
]
},
{
"version": "2022-10",
"actions": [
{
"action": "action_OTA_Ping"
},
{
"action": "action_OTA_Read"
},
{
"action": "action_OTA_HotelResNotif_GuestRequests"
}
]
}
]
}</Warning>
</Warnings>
<EchoData>{
"versions": [
{
"version": "2024-10",
"actions": [
{
"action": "action_OTA_Read"
},
{
"action": "action_OTA_HotelResNotif_GuestRequests"
},
{
"action": "action_OTA_Read"
}
]
},
{
"version": "2022-10",
"actions": [
{
"action": "action_OTA_Read"
},
{
"action": "action_OTA_Ping"
},
{
"action": "action_OTA_HotelResNotif_GuestRequests"
},
{
"action": "action_OTA_Read"
}
]
}
]
}</EchoData>
</OTA_PingRS>

View File

@@ -1,67 +0,0 @@
#!/usr/bin/env python3
"""
Quick test to demonstrate how the ServerCapabilities automatically
discovers implemented vs unimplemented actions.
"""
from alpine_bits_python.alpinebits_server import (
ServerCapabilities,
AlpineBitsAction,
AlpineBitsActionName,
Version,
AlpineBitsResponse,
HttpStatusCode,
)
import asyncio
class NewImplementedAction(AlpineBitsAction):
"""A new action that IS implemented."""
def __init__(self):
self.name = AlpineBitsActionName.OTA_HOTEL_DESCRIPTIVE_INFO_INFO
self.version = Version.V2024_10
async def handle(
self, action: str, request_xml: str, version: Version
) -> AlpineBitsResponse:
"""This action is implemented."""
return AlpineBitsResponse("Implemented!", HttpStatusCode.OK)
class NewUnimplementedAction(AlpineBitsAction):
"""A new action that is NOT implemented (no handle override)."""
def __init__(self):
self.name = AlpineBitsActionName.OTA_HOTEL_DESCRIPTIVE_CONTENT_NOTIF_INFO
self.version = Version.V2024_10
# Notice: No handle method override - will use default "not implemented"
async def main():
print("🔍 Testing Action Discovery Logic")
print("=" * 50)
# Create capabilities and see what gets discovered
capabilities = ServerCapabilities()
print("📋 Actions found by discovery:")
for action_name in capabilities.get_supported_actions():
print(f"{action_name}")
print(f"\n📊 Total discovered: {len(capabilities.get_supported_actions())}")
# Test the new implemented action
implemented_action = NewImplementedAction()
result = await implemented_action.handle("test", "<xml/>", Version.V2024_10)
print(f"\n🟢 NewImplementedAction result: {result.xml_content}")
# Test the unimplemented action (should use default behavior)
unimplemented_action = NewUnimplementedAction()
result = await unimplemented_action.handle("test", "<xml/>", Version.V2024_10)
print(f"🔴 NewUnimplementedAction result: {result.xml_content}")
if __name__ == "__main__":
asyncio.run(main())

14
uv.lock generated
View File

@@ -27,6 +27,7 @@ dependencies = [
{ name = "httpx" }, { name = "httpx" },
{ name = "lxml" }, { name = "lxml" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "redis" }, { name = "redis" },
{ name = "ruff" }, { name = "ruff" },
{ name = "slowapi" }, { name = "slowapi" },
@@ -47,6 +48,7 @@ requires-dist = [
{ name = "httpx", specifier = ">=0.28.1" }, { name = "httpx", specifier = ">=0.28.1" },
{ name = "lxml", specifier = ">=6.0.1" }, { name = "lxml", specifier = ">=6.0.1" },
{ name = "pytest", specifier = ">=8.4.2" }, { name = "pytest", specifier = ">=8.4.2" },
{ name = "pytest-asyncio", specifier = ">=1.2.0" },
{ name = "redis", specifier = ">=6.4.0" }, { name = "redis", specifier = ">=6.4.0" },
{ name = "ruff", specifier = ">=0.13.1" }, { name = "ruff", specifier = ">=0.13.1" },
{ name = "slowapi", specifier = ">=0.1.9" }, { name = "slowapi", specifier = ">=0.1.9" },
@@ -559,6 +561,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
] ]
[[package]]
name = "pytest-asyncio"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" },
]
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.1.1" version = "1.1.1"