Introduces the webhook egress foundation — a new WebhookSubscription table, admin-gated CRUD under /api/v1/webhooks, and the shared delivery client that both the test-ping route and the upcoming worker will use. No worker yet; this commit is API + model + client only. Simple-mode enum (AttackerDetail / DeckyStatus / SystemStatus) expands to bus-topic patterns at the router layer; storage is always the raw pattern list. Advanced mode lets admins supply raw NATS-style patterns directly. Filter-at-subscribe: the worker (next commit) will subscribe to the union of patterns across enabled subscriptions. Delivery client handles HMAC-SHA256 signing (X-DECNET-Signature), retry on 429/5xx/network errors with jittered backoff, no-retry on 4xx. Secrets never leave the server on GET/LIST — only the create response carries the secret for copy-out. CRUD routes publish WEBHOOK_SUBSCRIPTIONS_CHANGED on the bus after every mutation so the (future) worker can hot-reload. Opens DEBT-037 for the deferred items (circuit breaker, dead-letter, batch delivery, payload templates, secret-at-rest).
50 lines
1.5 KiB
Python
50 lines
1.5 KiB
Python
"""Unit tests for decnet.webhook.enums — simple→patterns expansion."""
|
|
from decnet.webhook.enums import (
|
|
SIMPLE_EVENT_PATTERNS,
|
|
expand_simple_events,
|
|
merge_patterns,
|
|
)
|
|
|
|
|
|
def test_simple_event_patterns_covers_three_families():
|
|
assert set(SIMPLE_EVENT_PATTERNS) == {
|
|
"AttackerDetail",
|
|
"DeckyStatus",
|
|
"SystemStatus",
|
|
}
|
|
|
|
|
|
def test_expand_single_event():
|
|
assert expand_simple_events(["AttackerDetail"]) == ["attacker.>"]
|
|
|
|
|
|
def test_expand_multiple_events_concatenates():
|
|
out = expand_simple_events(["AttackerDetail", "DeckyStatus"])
|
|
assert out == ["attacker.>", "decky.*.state", "decky.*.traffic"]
|
|
|
|
|
|
def test_expand_unknown_event_dropped_silently():
|
|
# The Literal type on the router rejects unknowns; this guards against
|
|
# programmer error, not user input.
|
|
assert expand_simple_events(["NotAThing"]) == []
|
|
|
|
|
|
def test_merge_dedups_overlap():
|
|
merged = merge_patterns(["AttackerDetail"], ["attacker.>", "custom.>"])
|
|
assert merged == ["attacker.>", "custom.>"]
|
|
|
|
|
|
def test_merge_preserves_order_simple_first():
|
|
merged = merge_patterns(["SystemStatus"], ["attacker.>", "decky.*.state"])
|
|
assert merged == ["system.>", "attacker.>", "decky.*.state"]
|
|
|
|
|
|
def test_merge_empty_lists_returns_empty():
|
|
assert merge_patterns([], []) == []
|
|
assert merge_patterns(None, None) == []
|
|
|
|
|
|
def test_merge_drops_empty_strings_and_non_strings():
|
|
merged = merge_patterns([], ["", "attacker.>", None]) # type: ignore[list-item]
|
|
assert merged == ["attacker.>"]
|