Merging schema_extension #9
@@ -11,7 +11,7 @@ import re
|
|||||||
from abc import ABC
|
from abc import ABC
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum, IntEnum
|
from enum import Enum
|
||||||
from typing import Any, Optional, override
|
from typing import Any, Optional, override
|
||||||
|
|
||||||
from xsdata.formats.dataclass.serializers.config import SerializerConfig
|
from xsdata.formats.dataclass.serializers.config import SerializerConfig
|
||||||
@@ -23,6 +23,7 @@ from alpine_bits_python.alpine_bits_helpers import (
|
|||||||
)
|
)
|
||||||
from alpine_bits_python.logging_config import get_logger
|
from alpine_bits_python.logging_config import get_logger
|
||||||
|
|
||||||
|
from .const import HttpStatusCode
|
||||||
from .db import Customer, Reservation
|
from .db import Customer, Reservation
|
||||||
from .generated.alpinebits import (
|
from .generated.alpinebits import (
|
||||||
OtaNotifReportRq,
|
OtaNotifReportRq,
|
||||||
@@ -38,15 +39,6 @@ from .reservation_service import ReservationService
|
|||||||
_LOGGER = get_logger(__name__)
|
_LOGGER = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class HttpStatusCode(IntEnum):
|
|
||||||
"""Allowed HTTP status codes for AlpineBits responses."""
|
|
||||||
|
|
||||||
OK = 200
|
|
||||||
BAD_REQUEST = 400
|
|
||||||
UNAUTHORIZED = 401
|
|
||||||
INTERNAL_SERVER_ERROR = 500
|
|
||||||
|
|
||||||
|
|
||||||
def dump_json_for_xml(json_content: Any) -> str:
|
def dump_json_for_xml(json_content: Any) -> str:
|
||||||
"""Dump JSON content as a pretty-printed string for embedding in XML.
|
"""Dump JSON content as a pretty-printed string for embedding in XML.
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ from .alpinebits_server import (
|
|||||||
)
|
)
|
||||||
from .auth import generate_unique_id, validate_api_key
|
from .auth import generate_unique_id, validate_api_key
|
||||||
from .config_loader import load_config
|
from .config_loader import load_config
|
||||||
|
from .const import HttpStatusCode
|
||||||
from .customer_service import CustomerService
|
from .customer_service import CustomerService
|
||||||
from .db import Base, get_database_url
|
from .db import Base, get_database_url
|
||||||
from .db import Customer as DBCustomer
|
from .db import Customer as DBCustomer
|
||||||
@@ -147,7 +148,7 @@ async def push_listener(customer: DBCustomer, reservation: DBReservation, hotel)
|
|||||||
version=Version.V2024_10,
|
version=Version.V2024_10,
|
||||||
)
|
)
|
||||||
|
|
||||||
if request.status_code != 200:
|
if request.status_code != HttpStatusCode.OK:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Failed to generate push request for hotel %s, reservation %s: %s",
|
"Failed to generate push request for hotel %s, reservation %s: %s",
|
||||||
hotel_id,
|
hotel_id,
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
|
from enum import IntEnum
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
|
|
||||||
|
class HttpStatusCode(IntEnum):
|
||||||
|
"""Allowed HTTP status codes for AlpineBits responses."""
|
||||||
|
|
||||||
|
OK = 200
|
||||||
|
BAD_REQUEST = 400
|
||||||
|
UNAUTHORIZED = 401
|
||||||
|
INTERNAL_SERVER_ERROR = 500
|
||||||
|
|
||||||
|
|
||||||
RESERVATION_ID_TYPE: str = (
|
RESERVATION_ID_TYPE: str = (
|
||||||
"13" # Default reservation ID type for Reservation. 14 would be cancellation
|
"13" # Default reservation ID type for Reservation. 14 would be cancellation
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,14 +16,12 @@ from xsdata_pydantic.bindings import XmlParser, XmlSerializer
|
|||||||
|
|
||||||
from alpine_bits_python.alpine_bits_helpers import create_res_retrieve_response
|
from alpine_bits_python.alpine_bits_helpers import create_res_retrieve_response
|
||||||
from alpine_bits_python.alpinebits_server import AlpineBitsClientInfo, AlpineBitsServer
|
from alpine_bits_python.alpinebits_server import AlpineBitsClientInfo, AlpineBitsServer
|
||||||
|
from alpine_bits_python.const import HttpStatusCode
|
||||||
from alpine_bits_python.db import AckedRequest, Base, Customer, Reservation
|
from alpine_bits_python.db import AckedRequest, Base, Customer, Reservation
|
||||||
from alpine_bits_python.generated import OtaReadRq
|
from alpine_bits_python.generated import OtaReadRq
|
||||||
from alpine_bits_python.generated.alpinebits import OtaResRetrieveRs
|
from alpine_bits_python.generated.alpinebits import OtaResRetrieveRs
|
||||||
from alpine_bits_python.schemas import ReservationData
|
from alpine_bits_python.schemas import ReservationData
|
||||||
|
|
||||||
# HTTP status code constants
|
|
||||||
HTTP_OK = 200
|
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture
|
@pytest_asyncio.fixture
|
||||||
async def test_db_engine():
|
async def test_db_engine():
|
||||||
@@ -558,7 +556,7 @@ class TestAcknowledgments:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response is not None
|
assert response is not None
|
||||||
assert response.status_code == HTTP_OK
|
assert response.status_code == HttpStatusCode.OK
|
||||||
assert response.xml_content is not None
|
assert response.xml_content is not None
|
||||||
|
|
||||||
# Verify response contains reservation data
|
# Verify response contains reservation data
|
||||||
@@ -609,7 +607,7 @@ class TestAcknowledgments:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert ack_response is not None
|
assert ack_response is not None
|
||||||
assert ack_response.status_code == HTTP_OK
|
assert ack_response.status_code == HttpStatusCode.OK
|
||||||
assert "OTA_NotifReportRS" in ack_response.xml_content
|
assert "OTA_NotifReportRS" in ack_response.xml_content
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -920,7 +918,7 @@ class TestAcknowledgments:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response is not None
|
assert response is not None
|
||||||
assert response.status_code == HTTP_OK
|
assert response.status_code == HttpStatusCode.OK
|
||||||
|
|
||||||
# Parse response to verify both reservations are returned
|
# Parse response to verify both reservations are returned
|
||||||
parser = XmlParser()
|
parser = XmlParser()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import pytest
|
|||||||
from xsdata_pydantic.bindings import XmlParser
|
from xsdata_pydantic.bindings import XmlParser
|
||||||
|
|
||||||
from alpine_bits_python.alpinebits_server import AlpineBitsClientInfo, AlpineBitsServer
|
from alpine_bits_python.alpinebits_server import AlpineBitsClientInfo, AlpineBitsServer
|
||||||
|
from alpine_bits_python.const import HttpStatusCode
|
||||||
from alpine_bits_python.generated.alpinebits import OtaPingRs
|
from alpine_bits_python.generated.alpinebits import OtaPingRs
|
||||||
|
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ async def test_ping_action_response_success():
|
|||||||
client_info=client_info,
|
client_info=client_info,
|
||||||
version="2024-10",
|
version="2024-10",
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
assert "<OTA_PingRS" in response.xml_content
|
assert "<OTA_PingRS" in response.xml_content
|
||||||
assert "<Success" in response.xml_content
|
assert "<Success" in response.xml_content
|
||||||
assert "Version=" in response.xml_content
|
assert "Version=" in response.xml_content
|
||||||
@@ -78,7 +79,7 @@ async def test_ping_action_response_version_arbitrary():
|
|||||||
client_info=client_info,
|
client_info=client_info,
|
||||||
version="2022-10",
|
version="2022-10",
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
assert "<OTA_PingRS" in response.xml_content
|
assert "<OTA_PingRS" in response.xml_content
|
||||||
assert "Version=" in response.xml_content
|
assert "Version=" in response.xml_content
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from fastapi.testclient import TestClient
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
from alpine_bits_python.api import app
|
from alpine_bits_python.api import app
|
||||||
|
from alpine_bits_python.const import HttpStatusCode
|
||||||
from alpine_bits_python.db import Base, Customer, Reservation
|
from alpine_bits_python.db import Base, Customer, Reservation
|
||||||
|
|
||||||
|
|
||||||
@@ -159,7 +160,7 @@ class TestHealthEndpoints:
|
|||||||
def test_root_endpoint(self, client):
|
def test_root_endpoint(self, client):
|
||||||
"""Test GET / returns health status."""
|
"""Test GET / returns health status."""
|
||||||
response = client.get("/api/")
|
response = client.get("/api/")
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["message"] == "Wix Form Handler API is running"
|
assert data["message"] == "Wix Form Handler API is running"
|
||||||
assert "timestamp" in data
|
assert "timestamp" in data
|
||||||
@@ -169,7 +170,7 @@ class TestHealthEndpoints:
|
|||||||
def test_health_check_endpoint(self, client):
|
def test_health_check_endpoint(self, client):
|
||||||
"""Test GET /api/health returns healthy status."""
|
"""Test GET /api/health returns healthy status."""
|
||||||
response = client.get("/api/health")
|
response = client.get("/api/health")
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["status"] == "healthy"
|
assert data["status"] == "healthy"
|
||||||
assert data["service"] == "wix-form-handler"
|
assert data["service"] == "wix-form-handler"
|
||||||
@@ -179,7 +180,7 @@ class TestHealthEndpoints:
|
|||||||
def test_landing_page(self, client):
|
def test_landing_page(self, client):
|
||||||
"""Test GET / (landing page) returns HTML."""
|
"""Test GET / (landing page) returns HTML."""
|
||||||
response = client.get("/")
|
response = client.get("/")
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
assert "text/html" in response.headers["content-type"]
|
assert "text/html" in response.headers["content-type"]
|
||||||
assert "99tales" in response.text or "Construction" in response.text
|
assert "99tales" in response.text or "Construction" in response.text
|
||||||
|
|
||||||
@@ -191,7 +192,7 @@ class TestWixWebhookEndpoint:
|
|||||||
"""Test successful Wix form submission."""
|
"""Test successful Wix form submission."""
|
||||||
response = client.post("/api/webhook/wix-form", json=sample_wix_form_data)
|
response = client.post("/api/webhook/wix-form", json=sample_wix_form_data)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["status"] == "success"
|
assert data["status"] == "success"
|
||||||
assert "timestamp" in data
|
assert "timestamp" in data
|
||||||
@@ -201,7 +202,7 @@ class TestWixWebhookEndpoint:
|
|||||||
):
|
):
|
||||||
"""Test that webhook creates customer and reservation in database."""
|
"""Test that webhook creates customer and reservation in database."""
|
||||||
response = client.post("/api/webhook/wix-form", json=sample_wix_form_data)
|
response = client.post("/api/webhook/wix-form", json=sample_wix_form_data)
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
|
|
||||||
# Verify data was saved to database
|
# Verify data was saved to database
|
||||||
# Use the client's app state engine, not a separate test_db_engine
|
# Use the client's app state engine, not a separate test_db_engine
|
||||||
@@ -251,14 +252,14 @@ class TestWixWebhookEndpoint:
|
|||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/api/webhook/wix-form", json=minimal_data)
|
response = client.post("/api/webhook/wix-form", json=minimal_data)
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["status"] == "success"
|
assert data["status"] == "success"
|
||||||
|
|
||||||
def test_wix_webhook_test_endpoint(self, client, sample_wix_form_data):
|
def test_wix_webhook_test_endpoint(self, client, sample_wix_form_data):
|
||||||
"""Test the test endpoint works identically."""
|
"""Test the test endpoint works identically."""
|
||||||
response = client.post("/api/webhook/wix-form/test", json=sample_wix_form_data)
|
response = client.post("/api/webhook/wix-form/test", json=sample_wix_form_data)
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["status"] == "success"
|
assert data["status"] == "success"
|
||||||
|
|
||||||
@@ -285,7 +286,7 @@ class TestWixWebhookEndpoint:
|
|||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/api/webhook/wix-form", json=first_submission)
|
response = client.post("/api/webhook/wix-form", json=first_submission)
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
|
|
||||||
# Second submission with same contact_id but different data
|
# Second submission with same contact_id but different data
|
||||||
second_submission = {
|
second_submission = {
|
||||||
@@ -310,7 +311,7 @@ class TestWixWebhookEndpoint:
|
|||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/api/webhook/wix-form", json=second_submission)
|
response = client.post("/api/webhook/wix-form", json=second_submission)
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
|
|
||||||
# Verify only one customer exists with updated information
|
# Verify only one customer exists with updated information
|
||||||
async def check_db():
|
async def check_db():
|
||||||
@@ -356,26 +357,20 @@ class TestGenericWebhookEndpoint:
|
|||||||
"""Test successful generic webhook submission with real form data."""
|
"""Test successful generic webhook submission with real form data."""
|
||||||
unique_id = uuid.uuid4().hex[:8]
|
unique_id = uuid.uuid4().hex[:8]
|
||||||
test_data = {
|
test_data = {
|
||||||
"hotel_data": {
|
"hotel_data": {"hotelname": "Bemelmans", "hotelcode": "39054_001"},
|
||||||
"hotelname": "Bemelmans",
|
|
||||||
"hotelcode": "39054_001"
|
|
||||||
},
|
|
||||||
"form_data": {
|
"form_data": {
|
||||||
"sprache": "it",
|
"sprache": "it",
|
||||||
"anreise": "14.10.2025",
|
"anreise": "14.10.2025",
|
||||||
"abreise": "15.10.2025",
|
"abreise": "15.10.2025",
|
||||||
"erwachsene": "1",
|
"erwachsene": "1",
|
||||||
"kinder": "2",
|
"kinder": "2",
|
||||||
"alter": {
|
"alter": {"1": "2", "2": "4"},
|
||||||
"1": "2",
|
|
||||||
"2": "4"
|
|
||||||
},
|
|
||||||
"anrede": "Herr",
|
"anrede": "Herr",
|
||||||
"name": "Armin",
|
"name": "Armin",
|
||||||
"nachname": "Wieser",
|
"nachname": "Wieser",
|
||||||
"mail": f"test.{unique_id}@example.com",
|
"mail": f"test.{unique_id}@example.com",
|
||||||
"tel": "+391234567890",
|
"tel": "+391234567890",
|
||||||
"nachricht": "Test message"
|
"nachricht": "Test message",
|
||||||
},
|
},
|
||||||
"tracking_data": {
|
"tracking_data": {
|
||||||
"utm_source": "ig",
|
"utm_source": "ig",
|
||||||
@@ -383,27 +378,27 @@ class TestGenericWebhookEndpoint:
|
|||||||
"utm_campaign": "Conversions_Apartment_Bemelmans_ITA",
|
"utm_campaign": "Conversions_Apartment_Bemelmans_ITA",
|
||||||
"utm_content": "Grafik_1_Apartments_Bemelmans",
|
"utm_content": "Grafik_1_Apartments_Bemelmans",
|
||||||
"utm_term": "Cold_Traffic_Conversions_Apartment_Bemelmans_ITA",
|
"utm_term": "Cold_Traffic_Conversions_Apartment_Bemelmans_ITA",
|
||||||
"fbclid": "test_fbclid_123"
|
"fbclid": "test_fbclid_123",
|
||||||
},
|
},
|
||||||
"timestamp": "2025-10-14T12:20:08+02:00"
|
"timestamp": "2025-10-14T12:20:08+02:00",
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/api/webhook/generic", json=test_data)
|
response = client.post("/api/webhook/generic", json=test_data)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["status"] == "success"
|
assert data["status"] == "success"
|
||||||
assert "timestamp" in data
|
assert "timestamp" in data
|
||||||
assert data["message"] == "Generic webhook data received and processed successfully"
|
assert (
|
||||||
|
data["message"]
|
||||||
|
== "Generic webhook data received and processed successfully"
|
||||||
|
)
|
||||||
|
|
||||||
def test_generic_webhook_creates_customer_and_reservation(self, client):
|
def test_generic_webhook_creates_customer_and_reservation(self, client):
|
||||||
"""Test that webhook creates customer and reservation in database."""
|
"""Test that webhook creates customer and reservation in database."""
|
||||||
unique_id = uuid.uuid4().hex[:8]
|
unique_id = uuid.uuid4().hex[:8]
|
||||||
test_data = {
|
test_data = {
|
||||||
"hotel_data": {
|
"hotel_data": {"hotelname": "Test Hotel", "hotelcode": "TEST123"},
|
||||||
"hotelname": "Test Hotel",
|
|
||||||
"hotelcode": "TEST123"
|
|
||||||
},
|
|
||||||
"form_data": {
|
"form_data": {
|
||||||
"sprache": "de",
|
"sprache": "de",
|
||||||
"anreise": "25.12.2025",
|
"anreise": "25.12.2025",
|
||||||
@@ -416,18 +411,18 @@ class TestGenericWebhookEndpoint:
|
|||||||
"nachname": "Schmidt",
|
"nachname": "Schmidt",
|
||||||
"mail": f"maria.{unique_id}@example.com",
|
"mail": f"maria.{unique_id}@example.com",
|
||||||
"tel": "+491234567890",
|
"tel": "+491234567890",
|
||||||
"nachricht": "Looking forward to our stay"
|
"nachricht": "Looking forward to our stay",
|
||||||
},
|
},
|
||||||
"tracking_data": {
|
"tracking_data": {
|
||||||
"utm_source": "google",
|
"utm_source": "google",
|
||||||
"utm_medium": "cpc",
|
"utm_medium": "cpc",
|
||||||
"utm_campaign": "winter2025"
|
"utm_campaign": "winter2025",
|
||||||
},
|
},
|
||||||
"timestamp": "2025-10-14T10:00:00Z"
|
"timestamp": "2025-10-14T10:00:00Z",
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/api/webhook/generic", json=test_data)
|
response = client.post("/api/webhook/generic", json=test_data)
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
|
|
||||||
# Verify data was saved to database
|
# Verify data was saved to database
|
||||||
async def check_db():
|
async def check_db():
|
||||||
@@ -441,8 +436,12 @@ class TestGenericWebhookEndpoint:
|
|||||||
customers = result.scalars().all()
|
customers = result.scalars().all()
|
||||||
# Find the customer we just created
|
# Find the customer we just created
|
||||||
customer = next(
|
customer = next(
|
||||||
(c for c in customers if c.email_address == f"maria.{unique_id}@example.com"),
|
(
|
||||||
None
|
c
|
||||||
|
for c in customers
|
||||||
|
if c.email_address == f"maria.{unique_id}@example.com"
|
||||||
|
),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
assert customer is not None, "Customer should be created"
|
assert customer is not None, "Customer should be created"
|
||||||
assert customer.given_name == "Maria"
|
assert customer.given_name == "Maria"
|
||||||
@@ -455,8 +454,7 @@ class TestGenericWebhookEndpoint:
|
|||||||
result = await session.execute(select(Reservation))
|
result = await session.execute(select(Reservation))
|
||||||
reservations = result.scalars().all()
|
reservations = result.scalars().all()
|
||||||
reservation = next(
|
reservation = next(
|
||||||
(r for r in reservations if r.customer_id == customer.id),
|
(r for r in reservations if r.customer_id == customer.id), None
|
||||||
None
|
|
||||||
)
|
)
|
||||||
assert reservation is not None, "Reservation should be created"
|
assert reservation is not None, "Reservation should be created"
|
||||||
assert reservation.hotel_code == "TEST123"
|
assert reservation.hotel_code == "TEST123"
|
||||||
@@ -464,13 +462,16 @@ class TestGenericWebhookEndpoint:
|
|||||||
assert reservation.num_adults == 2
|
assert reservation.num_adults == 2
|
||||||
assert reservation.num_children == 1
|
assert reservation.num_children == 1
|
||||||
# children_ages is stored as CSV string
|
# children_ages is stored as CSV string
|
||||||
children_ages = [int(age) for age in reservation.children_ages.split(",") if age]
|
children_ages = [
|
||||||
|
int(age) for age in reservation.children_ages.split(",") if age
|
||||||
|
]
|
||||||
assert len(children_ages) == 1
|
assert len(children_ages) == 1
|
||||||
assert children_ages[0] == 8
|
assert children_ages[0] == 8
|
||||||
assert reservation.utm_source == "google"
|
assert reservation.utm_source == "google"
|
||||||
assert reservation.utm_campaign == "winter2025"
|
assert reservation.utm_campaign == "winter2025"
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
asyncio.run(check_db())
|
asyncio.run(check_db())
|
||||||
|
|
||||||
def test_generic_webhook_missing_dates(self, client):
|
def test_generic_webhook_missing_dates(self, client):
|
||||||
@@ -481,10 +482,10 @@ class TestGenericWebhookEndpoint:
|
|||||||
"sprache": "de",
|
"sprache": "de",
|
||||||
"name": "John",
|
"name": "John",
|
||||||
"nachname": "Doe",
|
"nachname": "Doe",
|
||||||
"mail": "john@example.com"
|
"mail": "john@example.com",
|
||||||
# Missing anreise and abreise
|
# Missing anreise and abreise
|
||||||
},
|
},
|
||||||
"tracking_data": {}
|
"tracking_data": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/api/webhook/generic", json=test_data)
|
response = client.post("/api/webhook/generic", json=test_data)
|
||||||
@@ -503,9 +504,9 @@ class TestGenericWebhookEndpoint:
|
|||||||
"kinder": "0",
|
"kinder": "0",
|
||||||
"name": "Jane",
|
"name": "Jane",
|
||||||
"nachname": "Doe",
|
"nachname": "Doe",
|
||||||
"mail": "jane@example.com"
|
"mail": "jane@example.com",
|
||||||
},
|
},
|
||||||
"tracking_data": {}
|
"tracking_data": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/api/webhook/generic", json=test_data)
|
response = client.post("/api/webhook/generic", json=test_data)
|
||||||
@@ -523,26 +524,19 @@ class TestGenericWebhookEndpoint:
|
|||||||
"abreise": "15.08.2025",
|
"abreise": "15.08.2025",
|
||||||
"erwachsene": "2",
|
"erwachsene": "2",
|
||||||
"kinder": "3",
|
"kinder": "3",
|
||||||
"alter": {
|
"alter": {"1": "5", "2": "8", "3": "12"},
|
||||||
"1": "5",
|
|
||||||
"2": "8",
|
|
||||||
"3": "12"
|
|
||||||
},
|
|
||||||
"anrede": "--", # Should be filtered out
|
"anrede": "--", # Should be filtered out
|
||||||
"name": "Paolo",
|
"name": "Paolo",
|
||||||
"nachname": "Rossi",
|
"nachname": "Rossi",
|
||||||
"mail": f"paolo.{unique_id}@example.com",
|
"mail": f"paolo.{unique_id}@example.com",
|
||||||
"tel": "", # Empty phone
|
"tel": "", # Empty phone
|
||||||
"nachricht": ""
|
"nachricht": "",
|
||||||
},
|
},
|
||||||
"tracking_data": {
|
"tracking_data": {"fbclid": "test_fb_123", "gclid": "test_gc_456"},
|
||||||
"fbclid": "test_fb_123",
|
|
||||||
"gclid": "test_gc_456"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/api/webhook/generic", json=test_data)
|
response = client.post("/api/webhook/generic", json=test_data)
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
|
|
||||||
# Verify children ages were stored correctly
|
# Verify children ages were stored correctly
|
||||||
async def check_db():
|
async def check_db():
|
||||||
@@ -554,8 +548,7 @@ class TestGenericWebhookEndpoint:
|
|||||||
result = await session.execute(select(Reservation))
|
result = await session.execute(select(Reservation))
|
||||||
reservations = result.scalars().all()
|
reservations = result.scalars().all()
|
||||||
reservation = next(
|
reservation = next(
|
||||||
(r for r in reservations if r.hotel_code == "FAM001"),
|
(r for r in reservations if r.hotel_code == "FAM001"), None
|
||||||
None
|
|
||||||
)
|
)
|
||||||
assert reservation is not None
|
assert reservation is not None
|
||||||
assert reservation.num_children == 3
|
assert reservation.num_children == 3
|
||||||
@@ -571,14 +564,19 @@ class TestGenericWebhookEndpoint:
|
|||||||
result = await session.execute(select(Customer))
|
result = await session.execute(select(Customer))
|
||||||
customers = result.scalars().all()
|
customers = result.scalars().all()
|
||||||
customer = next(
|
customer = next(
|
||||||
(c for c in customers if c.email_address == f"paolo.{unique_id}@example.com"),
|
(
|
||||||
None
|
c
|
||||||
|
for c in customers
|
||||||
|
if c.email_address == f"paolo.{unique_id}@example.com"
|
||||||
|
),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
assert customer is not None
|
assert customer is not None
|
||||||
assert customer.phone is None # Empty phone should be None
|
assert customer.phone is None # Empty phone should be None
|
||||||
assert customer.name_prefix is None # -- should be filtered out
|
assert customer.name_prefix is None # -- should be filtered out
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
asyncio.run(check_db())
|
asyncio.run(check_db())
|
||||||
|
|
||||||
def test_generic_webhook_empty_payload(self, client):
|
def test_generic_webhook_empty_payload(self, client):
|
||||||
@@ -628,7 +626,7 @@ class TestAlpineBitsServerEndpoint:
|
|||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
assert "OTA_PingRS" in response.text
|
assert "OTA_PingRS" in response.text
|
||||||
assert "application/xml" in response.headers["content-type"]
|
assert "application/xml" in response.headers["content-type"]
|
||||||
assert "X-AlpineBits-Server-Version" in response.headers
|
assert "X-AlpineBits-Server-Version" in response.headers
|
||||||
@@ -639,7 +637,7 @@ class TestAlpineBitsServerEndpoint:
|
|||||||
|
|
||||||
response = client.post("/api/alpinebits/server-2024-10", data=form_data)
|
response = client.post("/api/alpinebits/server-2024-10", data=form_data)
|
||||||
|
|
||||||
assert response.status_code == 401
|
assert response.status_code == HttpStatusCode.UNAUTHORIZED
|
||||||
|
|
||||||
def test_alpinebits_invalid_credentials(self, client):
|
def test_alpinebits_invalid_credentials(self, client):
|
||||||
"""Test AlpineBits endpoint with invalid credentials."""
|
"""Test AlpineBits endpoint with invalid credentials."""
|
||||||
@@ -652,7 +650,7 @@ class TestAlpineBitsServerEndpoint:
|
|||||||
"/api/alpinebits/server-2024-10", data=form_data, headers=headers
|
"/api/alpinebits/server-2024-10", data=form_data, headers=headers
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 401
|
assert response.status_code == HttpStatusCode.UNAUTHORIZED
|
||||||
|
|
||||||
def test_alpinebits_missing_action(self, client, basic_auth_headers):
|
def test_alpinebits_missing_action(self, client, basic_auth_headers):
|
||||||
"""Test AlpineBits endpoint without action parameter."""
|
"""Test AlpineBits endpoint without action parameter."""
|
||||||
@@ -691,7 +689,7 @@ class TestAlpineBitsServerEndpoint:
|
|||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
assert "OTA_PingRS" in response.text
|
assert "OTA_PingRS" in response.text
|
||||||
|
|
||||||
|
|
||||||
@@ -715,7 +713,7 @@ class TestXMLUploadEndpoint:
|
|||||||
headers={**basic_auth_headers, "Content-Type": "application/xml"},
|
headers={**basic_auth_headers, "Content-Type": "application/xml"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
assert "Xml received" in response.text
|
assert "Xml received" in response.text
|
||||||
|
|
||||||
def test_xml_upload_gzip_compressed(self, client, basic_auth_headers):
|
def test_xml_upload_gzip_compressed(self, client, basic_auth_headers):
|
||||||
@@ -739,7 +737,7 @@ class TestXMLUploadEndpoint:
|
|||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
|
|
||||||
def test_xml_upload_missing_auth(self, client):
|
def test_xml_upload_missing_auth(self, client):
|
||||||
"""Test XML upload without authentication."""
|
"""Test XML upload without authentication."""
|
||||||
@@ -748,7 +746,7 @@ class TestXMLUploadEndpoint:
|
|||||||
content=b"<xml/>",
|
content=b"<xml/>",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 401
|
assert response.status_code == HttpStatusCode.UNAUTHORIZED
|
||||||
|
|
||||||
def test_xml_upload_invalid_path(self, client, basic_auth_headers):
|
def test_xml_upload_invalid_path(self, client, basic_auth_headers):
|
||||||
"""Test XML upload with path traversal attempt.
|
"""Test XML upload with path traversal attempt.
|
||||||
@@ -805,7 +803,7 @@ class TestAuthentication:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Should not be 401
|
# Should not be 401
|
||||||
assert response.status_code != 401
|
assert response.status_code != HttpStatusCode.UNAUTHORIZED
|
||||||
|
|
||||||
def test_basic_auth_missing_credentials(self, client):
|
def test_basic_auth_missing_credentials(self, client):
|
||||||
"""Test basic auth with missing credentials."""
|
"""Test basic auth with missing credentials."""
|
||||||
@@ -814,7 +812,7 @@ class TestAuthentication:
|
|||||||
data={"action": "OTA_Ping:Handshaking"},
|
data={"action": "OTA_Ping:Handshaking"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 401
|
assert response.status_code == HttpStatusCode.UNAUTHORIZED
|
||||||
|
|
||||||
def test_basic_auth_malformed_header(self, client):
|
def test_basic_auth_malformed_header(self, client):
|
||||||
"""Test basic auth with malformed Authorization header."""
|
"""Test basic auth with malformed Authorization header."""
|
||||||
@@ -839,7 +837,7 @@ class TestEventDispatcher:
|
|||||||
# The async task runs in background and doesn't affect response
|
# The async task runs in background and doesn't affect response
|
||||||
response = client.post("/api/webhook/wix-form", json=sample_wix_form_data)
|
response = client.post("/api/webhook/wix-form", json=sample_wix_form_data)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == HttpStatusCode.OK
|
||||||
# Event dispatcher is tested separately in its own test suite
|
# Event dispatcher is tested separately in its own test suite
|
||||||
|
|
||||||
|
|
||||||
@@ -902,7 +900,7 @@ class TestCORS:
|
|||||||
|
|
||||||
# TestClient returns 400 for OPTIONS requests
|
# TestClient returns 400 for OPTIONS requests
|
||||||
# In production, CORS middleware handles preflight correctly
|
# In production, CORS middleware handles preflight correctly
|
||||||
assert response.status_code in [200, 400, 405]
|
assert response.status_code in [HttpStatusCode.OK, 400, 405]
|
||||||
|
|
||||||
|
|
||||||
class TestRateLimiting:
|
class TestRateLimiting:
|
||||||
@@ -917,7 +915,7 @@ class TestRateLimiting:
|
|||||||
responses.append(response.status_code)
|
responses.append(response.status_code)
|
||||||
|
|
||||||
# All should succeed if under limit
|
# All should succeed if under limit
|
||||||
assert all(status == 200 for status in responses)
|
assert all(status == HttpStatusCode.OK for status in responses)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user