merge: testing → main (reconcile 2-week divergence)

This commit is contained in:
2026-04-28 18:36:00 -04:00
parent 499836c9e4
commit 862e4dbb31
1235 changed files with 160255 additions and 7996 deletions

View File

@@ -0,0 +1,145 @@
"""Unit tests for decnet.webhook.client — HMAC + retry policy."""
from __future__ import annotations
import hashlib
import hmac
import httpx
import pytest
from decnet.webhook.client import (
DeliveryResult,
SyntheticEvent,
build_payload,
deliver,
sign,
)
_EVENT = SyntheticEvent(
topic="attacker.observed",
type="first_sighting",
ts="2026-04-24T00:00:00+00:00",
id="11111111-1111-1111-1111-111111111111",
payload={"ip": "1.2.3.4"},
)
def _sub(url: str = "https://webhook.example/inbound", secret: str = "s" * 32) -> dict:
return {"uuid": "w1", "url": url, "secret": secret}
def test_sign_matches_known_vector():
body = b'{"hello":"world"}'
secret = "0123456789abcdef"
expected = (
"sha256="
+ hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
)
assert sign(secret, body) == expected
def test_build_payload_stable_key_order():
# Same input → same bytes → same HMAC, regardless of kwarg order.
b1 = build_payload(_EVENT)
b2 = build_payload(_EVENT)
assert b1 == b2
assert b'"topic":"attacker.observed"' in b1
assert b'"v":1' in b1
@pytest.mark.asyncio
async def test_deliver_success_on_2xx():
async def handler(request: httpx.Request) -> httpx.Response:
assert request.headers.get("X-DECNET-Signature", "").startswith("sha256=")
assert request.headers.get("X-DECNET-Event-Id") == _EVENT.id
return httpx.Response(200, json={"ok": True})
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport) as client:
result = await deliver(_sub(), _EVENT, retry_schedule=[], client=client)
assert result == DeliveryResult(ok=True, status_code=200, attempts=1)
@pytest.mark.asyncio
async def test_deliver_no_retry_on_4xx():
calls = {"n": 0}
async def handler(request: httpx.Request) -> httpx.Response:
calls["n"] += 1
return httpx.Response(400, text="bad body")
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport) as client:
result = await deliver(_sub(), _EVENT, retry_schedule=[1, 1, 1], client=client)
assert result.ok is False
assert result.status_code == 400
assert calls["n"] == 1 # no retry
@pytest.mark.asyncio
async def test_deliver_retries_on_429():
calls = {"n": 0}
async def handler(request: httpx.Request) -> httpx.Response:
calls["n"] += 1
if calls["n"] < 3:
return httpx.Response(429)
return httpx.Response(200)
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport) as client:
result = await deliver(_sub(), _EVENT, retry_schedule=[0, 0], client=client)
assert result.ok is True
assert result.attempts == 3
@pytest.mark.asyncio
async def test_deliver_retries_on_5xx_then_gives_up():
async def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(503)
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport) as client:
result = await deliver(_sub(), _EVENT, retry_schedule=[0, 0], client=client)
assert result.ok is False
assert result.status_code == 503
assert result.attempts == 3
@pytest.mark.asyncio
async def test_deliver_retries_on_connection_error():
async def handler(request: httpx.Request) -> httpx.Response:
raise httpx.ConnectError("boom")
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport) as client:
result = await deliver(_sub(), _EVENT, retry_schedule=[0], client=client)
assert result.ok is False
assert result.status_code is None
assert "ConnectError" in (result.error or "")
assert result.attempts == 2
@pytest.mark.asyncio
async def test_deliver_receiver_can_verify_signature():
"""End-to-end: receiver recomputes HMAC over the posted body and matches ours."""
sub = _sub(secret="deadbeefdeadbeef")
captured: dict = {}
async def handler(request: httpx.Request) -> httpx.Response:
captured["body"] = request.content
captured["sig"] = request.headers["X-DECNET-Signature"]
return httpx.Response(200)
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport) as client:
result = await deliver(sub, _EVENT, retry_schedule=[], client=client)
assert result.ok
expected = (
"sha256="
+ hmac.new(
sub["secret"].encode(), captured["body"], hashlib.sha256
).hexdigest()
)
assert captured["sig"] == expected