- Installierbares Package mit pyproject.toml - Client-Klasse mit Sync, Async (mit Polling), Webhook, Verify, Download - Typisierte Exception-Hierarchie - Webhook-Signatur-Verifikation (HMAC-SHA-256) - Pytest-Suite + Quickstart und Webhook-Receiver-Beispiel
181 lines
7.1 KiB
Python
181 lines
7.1 KiB
Python
"""Tests für hightrusted_capture.Client."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
import responses
|
|
|
|
from hightrusted_capture import (
|
|
Client,
|
|
InvalidApiKeyError,
|
|
QuotaExceededError,
|
|
RateLimitedError,
|
|
verify_webhook_signature,
|
|
)
|
|
from hightrusted_capture.client import DEFAULT_BASE_URL
|
|
|
|
|
|
@pytest.fixture
|
|
def client() -> Client:
|
|
return Client(api_key="ht_test_abc123", max_retries=0)
|
|
|
|
|
|
# ───────────────────────────────────────────────────────────────────
|
|
# Capture
|
|
# ───────────────────────────────────────────────────────────────────
|
|
@responses.activate
|
|
def test_capture_sync_returns_object(client: Client) -> None:
|
|
responses.add(
|
|
responses.POST,
|
|
f"{DEFAULT_BASE_URL}/captures",
|
|
json={
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"status": "ready",
|
|
"url": "https://example.com",
|
|
"verify_url": "https://verify.hightrusted.net/c/550e",
|
|
"created_at": "2026-04-25T14:32:08Z",
|
|
},
|
|
status=200,
|
|
)
|
|
|
|
capture = client.capture("https://example.com")
|
|
assert capture["status"] == "ready"
|
|
assert capture["id"] == "550e8400-e29b-41d4-a716-446655440000"
|
|
|
|
|
|
@responses.activate
|
|
def test_capture_sends_authorization_header(client: Client) -> None:
|
|
responses.add(
|
|
responses.POST,
|
|
f"{DEFAULT_BASE_URL}/captures",
|
|
json={"id": "x", "status": "ready", "created_at": "2026-04-25T00:00:00Z"},
|
|
status=200,
|
|
)
|
|
client.capture("https://example.com")
|
|
assert responses.calls[0].request.headers["Authorization"] == "Bearer ht_test_abc123"
|
|
|
|
|
|
@responses.activate
|
|
def test_capture_with_reference_and_viewport(client: Client) -> None:
|
|
responses.add(
|
|
responses.POST,
|
|
f"{DEFAULT_BASE_URL}/captures",
|
|
json={"id": "x", "status": "ready", "created_at": "2026-04-25T00:00:00Z"},
|
|
status=200,
|
|
)
|
|
client.capture(
|
|
"https://example.com",
|
|
reference="case-001",
|
|
viewport={"width": 1920, "height": 1080},
|
|
)
|
|
body = json.loads(responses.calls[0].request.body)
|
|
assert body["reference"] == "case-001"
|
|
assert body["viewport"]["width"] == 1920
|
|
|
|
|
|
# ───────────────────────────────────────────────────────────────────
|
|
# Verify
|
|
# ───────────────────────────────────────────────────────────────────
|
|
@responses.activate
|
|
def test_verify_by_id(client: Client) -> None:
|
|
responses.add(
|
|
responses.POST,
|
|
f"{DEFAULT_BASE_URL}/verify",
|
|
json={"valid": True, "capture_id": "550e"},
|
|
status=200,
|
|
)
|
|
result = client.verify(source="550e8400-e29b-41d4-a716-446655440000")
|
|
assert result["valid"] is True
|
|
|
|
|
|
def test_verify_requires_exactly_one_source(client: Client) -> None:
|
|
with pytest.raises(ValueError, match="Genau einer"):
|
|
client.verify()
|
|
with pytest.raises(ValueError, match="Genau einer"):
|
|
client.verify(source="x", pdf="y")
|
|
|
|
|
|
# ───────────────────────────────────────────────────────────────────
|
|
# Errors
|
|
# ───────────────────────────────────────────────────────────────────
|
|
@responses.activate
|
|
def test_invalid_api_key_raises(client: Client) -> None:
|
|
responses.add(
|
|
responses.POST,
|
|
f"{DEFAULT_BASE_URL}/captures",
|
|
json={"error": {"code": "invalid_api_key", "message": "Bad key", "request_id": "r1"}},
|
|
status=401,
|
|
)
|
|
with pytest.raises(InvalidApiKeyError) as exc_info:
|
|
client.capture("https://example.com")
|
|
assert exc_info.value.code == "invalid_api_key"
|
|
assert exc_info.value.request_id == "r1"
|
|
|
|
|
|
@responses.activate
|
|
def test_quota_exceeded_raises(client: Client) -> None:
|
|
responses.add(
|
|
responses.POST,
|
|
f"{DEFAULT_BASE_URL}/captures",
|
|
json={"error": {"code": "quota_exceeded", "message": "X", "request_id": "r1"}},
|
|
status=402,
|
|
)
|
|
with pytest.raises(QuotaExceededError):
|
|
client.capture("https://example.com")
|
|
|
|
|
|
@responses.activate
|
|
def test_rate_limit_includes_retry_after(client: Client) -> None:
|
|
responses.add(
|
|
responses.POST,
|
|
f"{DEFAULT_BASE_URL}/captures",
|
|
json={"error": {"code": "rate_limited", "message": "X", "request_id": "r1"}},
|
|
status=429,
|
|
headers={"Retry-After": "12"},
|
|
)
|
|
with pytest.raises(RateLimitedError) as exc_info:
|
|
client.capture("https://example.com")
|
|
assert exc_info.value.retry_after_seconds == 12
|
|
|
|
|
|
# ───────────────────────────────────────────────────────────────────
|
|
# Webhook-Signatur
|
|
# ───────────────────────────────────────────────────────────────────
|
|
def test_verify_webhook_signature_matches() -> None:
|
|
body = b'{"event":"capture.ready"}'
|
|
secret = "wh_secret_test"
|
|
# Erwartet: sha256=hmac(body, secret)
|
|
import hashlib
|
|
import hmac
|
|
|
|
sig = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
|
|
assert verify_webhook_signature(body, sig, secret) is True
|
|
|
|
|
|
def test_verify_webhook_signature_rejects_wrong_secret() -> None:
|
|
body = b'{"event":"capture.ready"}'
|
|
bad_sig = "sha256=" + "0" * 64
|
|
assert verify_webhook_signature(body, bad_sig, "secret") is False
|
|
|
|
|
|
def test_verify_webhook_signature_rejects_empty_inputs() -> None:
|
|
assert verify_webhook_signature(b"", "sha256=...", "secret") is False
|
|
assert verify_webhook_signature(b"x", "", "secret") is False
|
|
assert verify_webhook_signature(b"x", "sha256=...", "") is False
|
|
|
|
|
|
# ───────────────────────────────────────────────────────────────────
|
|
# Konfiguration
|
|
# ───────────────────────────────────────────────────────────────────
|
|
def test_client_requires_api_key(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.delenv("HIGHTRUSTED_API_KEY", raising=False)
|
|
with pytest.raises(ValueError, match="API-Key fehlt"):
|
|
Client()
|
|
|
|
|
|
def test_client_reads_api_key_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("HIGHTRUSTED_API_KEY", "ht_test_from_env")
|
|
client = Client()
|
|
assert client.api_key == "ht_test_from_env"
|