From caa63dfc44b383756bd223586ca22f0474de33bd Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:43:44 +0530 Subject: [PATCH] test(webhook): pin the dynamic-route secret guard Gate r2 findings 3 and 4. Mutating _dynamic_route_allowed to `if not effective_secret:` stayed green, because no test reached the hot-reload guard with a blank or non-string secret. The existing parametrized connect case now asserts the dynamic-route guard rejects the same secrets, and it goes red under that mutation (4/4). Also rename the test and its param to match what they cover (unusable, not only whitespace-only), and refresh the stale docstring to name _is_usable_secret. --- gateway/platforms/webhook.py | 4 ++-- tests/gateway/test_webhook_adapter.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/gateway/platforms/webhook.py b/gateway/platforms/webhook.py index 5acd2c87a5..fc886965a0 100644 --- a/gateway/platforms/webhook.py +++ b/gateway/platforms/webhook.py @@ -386,8 +386,8 @@ class WebhookAdapter(BasePlatformAdapter): return web.json_response({"status": "ok", "platform": "webhook"}) def _dynamic_route_allowed(self, name: str, route: dict) -> bool: - """An empty effective secret would make _handle_webhook skip HMAC validation → reject such - dynamic routes; INSECURE_NO_AUTH is loopback-only.""" + """Reject dynamic routes whose effective secret is missing, blank, or not a string + (_is_usable_secret); INSECURE_NO_AUTH is loopback-only.""" effective_secret = route.get("secret", self._global_secret) if not _is_usable_secret(effective_secret): logger.warning("[webhook] Dynamic route '%s' skipped: 'secret' is missing, blank, or not a string. " diff --git a/tests/gateway/test_webhook_adapter.py b/tests/gateway/test_webhook_adapter.py index 54cd343294..db0d1898f6 100644 --- a/tests/gateway/test_webhook_adapter.py +++ b/tests/gateway/test_webhook_adapter.py @@ -1252,11 +1252,13 @@ def test_route_profile_validation_fails_closed(): class TestBlankRouteSecretFailsClosed: - @pytest.mark.parametrize("blank", [" ", "\t", "\n", 123]) - def test_connect_rejects_a_whitespace_only_route_secret(self, blank): - adapter = _make_adapter(routes={"hook": {"secret": blank}}) + @pytest.mark.parametrize("secret", [" ", "\t", "\n", 123]) + def test_connect_rejects_an_unusable_route_secret(self, secret): + adapter = _make_adapter(routes={"hook": {"secret": secret}}) with pytest.raises(ValueError, match="missing, blank, or not a string"): asyncio.run(adapter.connect()) + # The hot-reload path applies the same guard to agent-created dynamic routes. + assert adapter._dynamic_route_allowed("hook", {"secret": secret}) is False @pytest.mark.parametrize(