python/tests/test_client.py
Stefan Schmidt-Egermann fa81aba944
feat: initial python content (v0.1.0)
- 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
2026-04-25 12:26:03 +02:00

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"