diff --git a/pyproject.toml b/pyproject.toml index e628069..073ea9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "httpx>=0.28.1", "lxml>=6.0.1", "pytest>=8.4.2", + "pytest-asyncio>=1.2.0", "redis>=6.4.0", "ruff>=0.13.1", "slowapi>=0.1.9", diff --git a/test/test_simplified_access.py b/test/test_alpine_bits_helper.py similarity index 99% rename from test/test_simplified_access.py rename to test/test_alpine_bits_helper.py index 6b1c96a..c098c50 100644 --- a/test/test_simplified_access.py +++ b/test/test_alpine_bits_helper.py @@ -3,10 +3,9 @@ from typing import Union import sys 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, CustomerFactory, ResGuestFactory, diff --git a/test/test_alpine_bits_server.py b/test/test_alpine_bits_server.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_alpinebits_server_ping.py b/test/test_alpinebits_server_ping.py new file mode 100644 index 0000000..3013c40 --- /dev/null +++ b/test/test_alpinebits_server_ping.py @@ -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 " + + + + + +{ + "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" + ] + } + ] + } + ] +} + + diff --git a/test/test_data/Handshake-OTA_PingRS.xml b/test/test_data/Handshake-OTA_PingRS.xml new file mode 100644 index 0000000..05fc21d --- /dev/null +++ b/test/test_data/Handshake-OTA_PingRS.xml @@ -0,0 +1,87 @@ + + + + + + + + { + "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" + } + ] + } + ] +} + + { + "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" + } + ] + } + ] +} + \ No newline at end of file diff --git a/test/test_discovery.py b/test/test_discovery.py deleted file mode 100644 index 2eda866..0000000 --- a/test/test_discovery.py +++ /dev/null @@ -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", "", 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", "", Version.V2024_10) - print(f"šŸ”“ NewUnimplementedAction result: {result.xml_content}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/uv.lock b/uv.lock index f867c49..b9286a4 100644 --- a/uv.lock +++ b/uv.lock @@ -27,6 +27,7 @@ dependencies = [ { name = "httpx" }, { name = "lxml" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "redis" }, { name = "ruff" }, { name = "slowapi" }, @@ -47,6 +48,7 @@ requires-dist = [ { name = "httpx", specifier = ">=0.28.1" }, { name = "lxml", specifier = ">=6.0.1" }, { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-asyncio", specifier = ">=1.2.0" }, { name = "redis", specifier = ">=6.4.0" }, { name = "ruff", specifier = ">=0.13.1" }, { 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" }, ] +[[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]] name = "python-dotenv" version = "1.1.1"