generated from coulomb/repo-seed
Some checks failed
ci / validate-registry (push) Has been cancelled
reuse_surface/hub/store.py: compose_state table tracking composed_at/stale, updated only on a *forced* recompose (refresh=true, webhook, future scheduled fallback) -- a plain GET still serves current best-effort data but never silently reports itself as freshly composed. reuse_surface/hub/webhooks.py: constant-time HMAC-SHA256 signature verification (fails closed on an empty/unconfigured secret) and path-only push-payload inspection (never parses file content, per design principle 2 -- webhook only decides whether to trigger a pull-based recompose). New endpoint POST /v1/webhooks/forgejo, accepting both X-Forgejo-Signature and X-Gitea-Signature headers since sibling repos migrate independently. GET /v1/federated and POST /v1/federated/compose now share an asyncio.Lock with the webhook so concurrent recompose triggers coalesce instead of overlapping. No separate /v1/recompose route was added -- POST /v1/federated/compose already did that job from earlier hub work; specs/FederationHubAPI.md now documents this explicitly instead of duplicating it. 28 new pytest cases (128 total pass). Live-verified: ran the actual hub service locally and sent a real HMAC-signed webhook over HTTP, confirming the full webhook-to-recompose path, signature rejection, and the irrelevant-path no-op. Does NOT deploy to the live reuse.coulomb.social hub -- that needs a container rebuild/push/k8s rollout, a separate production-deployment action out of scope here without explicit sign-off. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
83 lines
2.8 KiB
Python
83 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
|
|
from reuse_surface.hub.webhooks import push_touches_registry_index, verify_signature
|
|
|
|
SECRET = "shh"
|
|
|
|
|
|
def _sign(body: bytes, secret: str = SECRET) -> str:
|
|
return hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
|
|
|
|
|
|
def test_verify_signature_valid():
|
|
body = b'{"commits": []}'
|
|
assert verify_signature(SECRET, body, _sign(body)) is True
|
|
|
|
|
|
def test_verify_signature_wrong_secret():
|
|
body = b'{"commits": []}'
|
|
assert verify_signature("different-secret", body, _sign(body)) is False
|
|
|
|
|
|
def test_verify_signature_tampered_body():
|
|
body = b'{"commits": []}'
|
|
signature = _sign(body)
|
|
assert verify_signature(SECRET, b'{"commits": [1]}', signature) is False
|
|
|
|
|
|
def test_verify_signature_missing_signature():
|
|
assert verify_signature(SECRET, b"{}", None) is False
|
|
|
|
|
|
def test_verify_signature_empty_secret_rejects():
|
|
body = b"{}"
|
|
# Even if a caller accidentally computed a signature with an empty
|
|
# secret, verify_signature must not accept it -- an unconfigured
|
|
# secret is not the same as "verification not needed".
|
|
assert verify_signature("", body, _sign(body, secret="")) is False
|
|
|
|
|
|
def test_push_touches_registry_index_added():
|
|
payload = {"commits": [{"added": ["registry/indexes/capabilities.yaml"], "modified": [], "removed": []}]}
|
|
assert push_touches_registry_index(payload) is True
|
|
|
|
|
|
def test_push_touches_registry_index_modified():
|
|
payload = {"commits": [{"added": [], "modified": ["registry/indexes/federated.yaml"], "removed": []}]}
|
|
assert push_touches_registry_index(payload) is True
|
|
|
|
|
|
def test_push_touches_registry_index_removed():
|
|
payload = {"commits": [{"added": [], "modified": [], "removed": ["registry/indexes/capabilities.yaml"]}]}
|
|
assert push_touches_registry_index(payload) is True
|
|
|
|
|
|
def test_push_does_not_touch_registry_index():
|
|
payload = {"commits": [{"added": ["README.md"], "modified": ["src/main.py"], "removed": []}]}
|
|
assert push_touches_registry_index(payload) is False
|
|
|
|
|
|
def test_push_touches_registry_index_across_multiple_commits():
|
|
payload = {
|
|
"commits": [
|
|
{"added": ["README.md"], "modified": [], "removed": []},
|
|
{"added": [], "modified": ["registry/indexes/capabilities.yaml"], "removed": []},
|
|
]
|
|
}
|
|
assert push_touches_registry_index(payload) is True
|
|
|
|
|
|
def test_push_touches_registry_index_empty_commits():
|
|
assert push_touches_registry_index({"commits": []}) is False
|
|
assert push_touches_registry_index({}) is False
|
|
|
|
|
|
def test_push_touches_registry_index_ignores_similarly_named_paths():
|
|
# a path that merely starts with "registry" but isn't under
|
|
# registry/indexes/ must not match
|
|
payload = {"commits": [{"added": ["registry/capabilities/capability.foo.md"], "modified": [], "removed": []}]}
|
|
assert push_touches_registry_index(payload) is False
|