feat: Enhance Booklooker client with webhook signature verification, idempotency handling, and retry logic

This commit is contained in:
claudi 2026-04-16 14:58:18 +02:00
parent 1d8ee1bba6
commit f2e5774204
6 changed files with 232 additions and 50 deletions

50
tests/test_resilience.py Normal file
View file

@ -0,0 +1,50 @@
import hashlib
import hmac
import httpx
import pytest
from booklooker_client import BooklookerConfig, BooklookerWebhookHelper, SyncBooklookerClient
def test_sync_client_retries_transient_timeout() -> None:
attempts = {"count": 0}
def handler(request: httpx.Request) -> httpx.Response:
attempts["count"] += 1
if attempts["count"] == 1:
raise httpx.ReadTimeout("temporary timeout", request=request)
return httpx.Response(200, json={"status": "OK", "returnValue": "REST_API_TOKEN"})
config = BooklookerConfig(api_key="demo", max_retries=1, retry_backoff_seconds=0)
client = SyncBooklookerClient(config)
client._http = httpx.Client(transport=httpx.MockTransport(handler), base_url=config.base_url)
try:
token = client.authenticate()
finally:
client.close()
assert token.token == "REST_API_TOKEN"
assert attempts["count"] == 2
def test_webhook_signature_verification() -> None:
payload = b'{"event_type":"order.created","event_id":"evt-10"}'
secret = "top-secret"
signature = hmac.new(secret.encode("utf-8"), payload, hashlib.sha256).hexdigest()
helper = BooklookerWebhookHelper(webhook_secret=secret)
assert helper.verify_signature(payload, signature) is True
def test_import_file_rejects_oversized_input(tmp_path) -> None:
file_path = tmp_path / "payload.bin"
file_path.write_bytes(b"1234")
client = SyncBooklookerClient(BooklookerConfig(api_key="demo", max_upload_size_bytes=1))
try:
with pytest.raises(ValueError):
client.import_file(file_path, data_type=0)
finally:
client.close()

View file

@ -1,4 +1,4 @@
from booklooker_client import BooklookerWebhookHelper
from booklooker_client import BooklookerWebhookHelper, InMemoryIdempotencyStore
from booklooker_client.models.order import OrderBatch, OrderRecord
@ -39,3 +39,9 @@ def test_duplicate_detection() -> None:
assert first.resource_type == "order"
assert second.resource_type == "duplicate"
assert second.enriched_data == {"duplicate": True}
def test_idempotency_store_ttl_expiry() -> None:
store = InMemoryIdempotencyStore(ttl_seconds=0)
store.mark_seen("evt-expire")
assert store.has_seen("evt-expire") is False