Fixed some tests and added schemas
This commit is contained in:
218
tests/test_schemas_webhook.py
Normal file
218
tests/test_schemas_webhook.py
Normal file
@@ -0,0 +1,218 @@
|
||||
"""Tests for webhook-related Pydantic schemas."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from alpine_bits_python.const import WebhookStatus
|
||||
from alpine_bits_python.schemas import (
|
||||
HotelData,
|
||||
WebhookEndpointData,
|
||||
WebhookRequestData,
|
||||
)
|
||||
|
||||
|
||||
class TestHotelData:
|
||||
"""Tests for HotelData schema."""
|
||||
|
||||
def test_valid_hotel_data(self):
|
||||
"""Test creating a valid HotelData instance."""
|
||||
data = HotelData(
|
||||
hotel_id="hotel123",
|
||||
hotel_name="Test Hotel",
|
||||
username="admin",
|
||||
password_hash="hashed_password_123",
|
||||
)
|
||||
assert data.hotel_id == "hotel123"
|
||||
assert data.hotel_name == "Test Hotel"
|
||||
assert data.username == "admin"
|
||||
assert data.password_hash == "hashed_password_123"
|
||||
assert data.is_active is True
|
||||
assert isinstance(data.created_at, datetime)
|
||||
|
||||
def test_whitespace_stripping(self):
|
||||
"""Test that whitespace is stripped from string fields."""
|
||||
data = HotelData(
|
||||
hotel_id=" hotel123 ",
|
||||
hotel_name=" Test Hotel ",
|
||||
username=" admin ",
|
||||
password_hash="hashed_password_123",
|
||||
)
|
||||
assert data.hotel_id == "hotel123"
|
||||
assert data.hotel_name == "Test Hotel"
|
||||
assert data.username == "admin"
|
||||
|
||||
def test_optional_fields(self):
|
||||
"""Test that optional fields can be None."""
|
||||
data = HotelData(
|
||||
hotel_id="hotel123",
|
||||
hotel_name="Test Hotel",
|
||||
username="admin",
|
||||
password_hash="hashed_password_123",
|
||||
meta_account_id=None,
|
||||
google_account_id=None,
|
||||
)
|
||||
assert data.meta_account_id is None
|
||||
assert data.google_account_id is None
|
||||
|
||||
|
||||
class TestWebhookEndpointData:
|
||||
"""Tests for WebhookEndpointData schema."""
|
||||
|
||||
def test_valid_webhook_endpoint(self):
|
||||
"""Test creating a valid WebhookEndpointData instance."""
|
||||
data = WebhookEndpointData(
|
||||
hotel_id="hotel123",
|
||||
webhook_secret="secret_abc123",
|
||||
webhook_type="wix_form",
|
||||
)
|
||||
assert data.hotel_id == "hotel123"
|
||||
assert data.webhook_secret == "secret_abc123"
|
||||
assert data.webhook_type == "wix_form"
|
||||
assert data.is_enabled is True
|
||||
assert isinstance(data.created_at, datetime)
|
||||
|
||||
def test_webhook_endpoint_with_description(self):
|
||||
"""Test WebhookEndpointData with optional description."""
|
||||
data = WebhookEndpointData(
|
||||
hotel_id="hotel123",
|
||||
webhook_secret="secret_abc123",
|
||||
webhook_type="generic",
|
||||
description="Main booking form",
|
||||
)
|
||||
assert data.description == "Main booking form"
|
||||
|
||||
def test_whitespace_stripping(self):
|
||||
"""Test that whitespace is stripped from string fields."""
|
||||
data = WebhookEndpointData(
|
||||
hotel_id=" hotel123 ",
|
||||
webhook_secret=" secret_abc123 ",
|
||||
webhook_type=" wix_form ",
|
||||
)
|
||||
assert data.hotel_id == "hotel123"
|
||||
assert data.webhook_secret == "secret_abc123"
|
||||
assert data.webhook_type == "wix_form"
|
||||
|
||||
|
||||
class TestWebhookRequestData:
|
||||
"""Tests for WebhookRequestData schema."""
|
||||
|
||||
def test_auto_calculate_payload_hash(self):
|
||||
"""Test that payload_hash is auto-calculated from payload_json."""
|
||||
payload = {"name": "John", "email": "john@example.com"}
|
||||
data = WebhookRequestData(payload_json=payload)
|
||||
|
||||
# Verify hash was calculated
|
||||
assert data.payload_hash is not None
|
||||
assert len(data.payload_hash) == 64 # SHA256 produces 64 hex chars
|
||||
|
||||
# Verify it matches the expected hash (same algorithm as api.py)
|
||||
payload_json_str = json.dumps(payload, sort_keys=True)
|
||||
expected_hash = hashlib.sha256(payload_json_str.encode("utf-8")).hexdigest()
|
||||
assert data.payload_hash == expected_hash
|
||||
|
||||
def test_explicit_payload_hash(self):
|
||||
"""Test providing payload_hash explicitly (for purged payloads)."""
|
||||
explicit_hash = "a" * 64
|
||||
data = WebhookRequestData(
|
||||
payload_json=None,
|
||||
payload_hash=explicit_hash,
|
||||
)
|
||||
assert data.payload_hash == explicit_hash
|
||||
assert data.payload_json is None
|
||||
|
||||
def test_payload_hash_required(self):
|
||||
"""Test that payload_hash is required (either calculated or explicit)."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
WebhookRequestData(
|
||||
payload_json=None,
|
||||
payload_hash=None,
|
||||
)
|
||||
assert "payload_hash is required" in str(exc_info.value)
|
||||
|
||||
def test_consistent_hashing(self):
|
||||
"""Test that the same payload always produces the same hash."""
|
||||
payload = {"b": 2, "a": 1, "c": 3} # Unordered keys
|
||||
|
||||
data1 = WebhookRequestData(payload_json=payload.copy())
|
||||
data2 = WebhookRequestData(payload_json=payload.copy())
|
||||
|
||||
assert data1.payload_hash == data2.payload_hash
|
||||
|
||||
def test_default_status(self):
|
||||
"""Test that status defaults to PENDING."""
|
||||
data = WebhookRequestData(payload_json={"test": "data"})
|
||||
assert data.status == WebhookStatus.PENDING
|
||||
|
||||
def test_status_normalization(self):
|
||||
"""Test that status is normalized to WebhookStatus enum."""
|
||||
data = WebhookRequestData(
|
||||
payload_json={"test": "data"},
|
||||
status="completed", # String
|
||||
)
|
||||
assert data.status == WebhookStatus.COMPLETED
|
||||
assert isinstance(data.status, WebhookStatus)
|
||||
|
||||
def test_retry_count_default(self):
|
||||
"""Test that retry_count defaults to 0."""
|
||||
data = WebhookRequestData(payload_json={"test": "data"})
|
||||
assert data.retry_count == 0
|
||||
|
||||
def test_optional_foreign_keys(self):
|
||||
"""Test optional foreign key fields."""
|
||||
data = WebhookRequestData(
|
||||
payload_json={"test": "data"},
|
||||
webhook_endpoint_id=123,
|
||||
hotel_id="hotel456",
|
||||
)
|
||||
assert data.webhook_endpoint_id == 123
|
||||
assert data.hotel_id == "hotel456"
|
||||
|
||||
def test_result_tracking(self):
|
||||
"""Test result tracking fields."""
|
||||
data = WebhookRequestData(
|
||||
payload_json={"test": "data"},
|
||||
created_customer_id=1,
|
||||
created_reservation_id=2,
|
||||
)
|
||||
assert data.created_customer_id == 1
|
||||
assert data.created_reservation_id == 2
|
||||
|
||||
def test_purged_payload(self):
|
||||
"""Test representing a purged webhook request (after processing)."""
|
||||
explicit_hash = "b" * 64
|
||||
data = WebhookRequestData(
|
||||
payload_json=None,
|
||||
payload_hash=explicit_hash,
|
||||
status=WebhookStatus.COMPLETED,
|
||||
purged_at=datetime.now(),
|
||||
)
|
||||
assert data.payload_json is None
|
||||
assert data.payload_hash == explicit_hash
|
||||
assert data.status == WebhookStatus.COMPLETED
|
||||
assert data.purged_at is not None
|
||||
|
||||
def test_processing_metadata(self):
|
||||
"""Test processing tracking fields."""
|
||||
now = datetime.now()
|
||||
data = WebhookRequestData(
|
||||
payload_json={"test": "data"},
|
||||
status=WebhookStatus.PROCESSING,
|
||||
processing_started_at=now,
|
||||
)
|
||||
assert data.status == WebhookStatus.PROCESSING
|
||||
assert data.processing_started_at == now
|
||||
assert data.processing_completed_at is None
|
||||
|
||||
def test_request_metadata(self):
|
||||
"""Test request metadata fields."""
|
||||
data = WebhookRequestData(
|
||||
payload_json={"test": "data"},
|
||||
source_ip="192.168.1.1",
|
||||
user_agent="Mozilla/5.0",
|
||||
)
|
||||
assert data.source_ip == "192.168.1.1"
|
||||
assert data.user_agent == "Mozilla/5.0"
|
||||
@@ -7,6 +7,7 @@ This module tests:
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
@@ -22,6 +23,8 @@ from alpine_bits_python.api import app
|
||||
from alpine_bits_python.const import WebhookStatus
|
||||
from alpine_bits_python.db import Base, Reservation, WebhookRequest
|
||||
from alpine_bits_python.db_setup import reprocess_stuck_webhooks
|
||||
from alpine_bits_python.schemas import WebhookRequestData
|
||||
from alpine_bits_python.webhook_processor import initialize_webhook_processors, webhook_registry
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
@@ -165,23 +168,16 @@ class TestWebhookReprocessing:
|
||||
# Step 1: Process a webhook normally to create a reservation
|
||||
from alpine_bits_python.webhook_processor import process_wix_form_submission
|
||||
|
||||
test_data = {
|
||||
"data": {
|
||||
"submissionId": "STUCK-WEBHOOK-TEST-ID",
|
||||
"submissionTime": "2025-10-07T05:48:41.855Z",
|
||||
"contact": {
|
||||
"name": {"first": "Jane", "last": "Smith"},
|
||||
"email": "jane.smith@example.com",
|
||||
"phones": [{"e164Phone": "+9876543210"}],
|
||||
"locale": "en-US",
|
||||
"contactId": "contact-stuck-test",
|
||||
},
|
||||
"field:date_picker_a7c8": "2024-12-25",
|
||||
"field:date_picker_7e65": "2024-12-31",
|
||||
"field:number_7cf5": "2",
|
||||
"field:anzahl_kinder": "0",
|
||||
}
|
||||
}
|
||||
test_form_file = Path(__file__).parent / "test_data" / f"test_form{1}.json"
|
||||
|
||||
if not test_form_file.exists():
|
||||
pytest.skip(f"{test_form_file.name} not found")
|
||||
|
||||
# Load test form data
|
||||
with test_form_file.open() as f:
|
||||
test_data = json.load(f)
|
||||
|
||||
test_data["data"]["submissionId"] = "STUCK-WEBHOOK-TEST-ID" # Fixed ID for duplicate test
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await process_wix_form_submission(
|
||||
@@ -197,7 +193,7 @@ class TestWebhookReprocessing:
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
reservation = result.scalar_one_or_none()
|
||||
assert reservation is not None
|
||||
assert reservation is not None, "Reservation should exist"
|
||||
assert reservation.unique_id == "STUCK-WEBHOOK-TEST-ID"
|
||||
|
||||
# Step 3: Manually create a webhook request stuck in "processing" status
|
||||
@@ -230,16 +226,23 @@ class TestWebhookReprocessing:
|
||||
await session.flush()
|
||||
|
||||
# Create stuck webhook request with the SAME payload
|
||||
stuck_webhook = WebhookRequest(
|
||||
stuck_webhook_data = WebhookRequestData(
|
||||
webhook_endpoint_id=endpoint.id,
|
||||
hotel_id="HOTEL123",
|
||||
payload_json=test_data,
|
||||
status=WebhookStatus.PROCESSING, # Stuck in processing!
|
||||
created_at=datetime.now(UTC),
|
||||
)
|
||||
|
||||
stuck_webhook = WebhookRequest(**stuck_webhook_data.model_dump())
|
||||
session.add(stuck_webhook)
|
||||
await session.commit()
|
||||
|
||||
# initialize wix_form processor
|
||||
|
||||
initialize_webhook_processors()
|
||||
|
||||
|
||||
# Step 4: Run reprocessing (simulates app restart)
|
||||
await reprocess_stuck_webhooks(AsyncSessionLocal, test_config)
|
||||
|
||||
@@ -307,15 +310,14 @@ class TestWebhookReprocessingNeverBlocksStartup:
|
||||
session.add(endpoint)
|
||||
await session.flush()
|
||||
|
||||
# Create stuck webhook with INVALID data (missing required fields)
|
||||
stuck_webhook = WebhookRequest(
|
||||
webhook_endpoint_id=endpoint.id,
|
||||
webhook_request = WebhookRequestData(
|
||||
hotel_id="HOTEL123",
|
||||
payload_json={"data": {"invalid": "data"}}, # Missing required fields
|
||||
status=WebhookStatus.PROCESSING,
|
||||
created_at=datetime.now(UTC),
|
||||
payload_hash="invalidhash" # Add a dummy payload_hash to avoid integrity error
|
||||
status=WebhookStatus.PROCESSING
|
||||
)
|
||||
stuck_webhook = WebhookRequest(**webhook_request.model_dump())
|
||||
|
||||
|
||||
session.add(stuck_webhook) ## Cannot add the stuck webhook. Integrity Error payload_hash is missing
|
||||
await session.commit()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user