- webhook_receiver: always run hmac.compare_digest (drop the `not provided or` short-circuit) so the constant-time path is taken regardless of whether the Authorization header is present. - client/webhooks: modernise the new `auth_data` type hint to `dict[str, str] | None` per CLAUDE.md. - tests/client: rename `test_create_webhook_with_auth_headers` → `test_create_webhook_with_static_headers` and use `auth_method="header"` (NC's webhook_listeners only supports "none" and "header"; the previous "bearer" value was invalid). - auth/webhook_routes: extract `_register_preset_webhooks` from `enable_webhook_preset` so the auth-threading behaviour is testable without standing up a Starlette app + auth middleware. - tests/unit: new test_webhook_routes_register covering the helper with secret set / unset, and verifying ids round-trip in order. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
85 lines
3.0 KiB
Python
85 lines
3.0 KiB
Python
"""Unit tests for ``_register_preset_webhooks``.
|
|
|
|
The helper threads ``webhook_auth_pair()`` into each ``create_webhook``
|
|
call, so it is the integration point between the secret-resolution logic
|
|
and the OCS client. These tests verify the wiring without standing up a
|
|
full Starlette app.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from nextcloud_mcp_server.auth import webhook_routes
|
|
from nextcloud_mcp_server.auth.webhook_routes import _register_preset_webhooks
|
|
from nextcloud_mcp_server.client.webhooks import WebhooksClient
|
|
from nextcloud_mcp_server.config import Settings
|
|
from nextcloud_mcp_server.server.webhook_presets import get_preset
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
def _patch_secret(monkeypatch, secret: str | None) -> None:
|
|
monkeypatch.setattr(
|
|
webhook_routes,
|
|
"get_settings",
|
|
lambda: Settings(webhook_secret=secret),
|
|
)
|
|
|
|
|
|
def _make_webhooks_client(mocker, ids: list[int]):
|
|
"""Mock WebhooksClient.create_webhook to return one fake webhook per id."""
|
|
client = mocker.AsyncMock(spec=WebhooksClient)
|
|
client.create_webhook.side_effect = [{"id": i} for i in ids]
|
|
return client
|
|
|
|
|
|
async def test_register_threads_bearer_auth_when_secret_set(monkeypatch, mocker):
|
|
_patch_secret(monkeypatch, "supersecret")
|
|
preset = get_preset("notes_sync")
|
|
assert preset is not None
|
|
client = _make_webhooks_client(mocker, ids=[101, 102, 103])
|
|
|
|
registered = await _register_preset_webhooks(
|
|
client, preset, "https://mcp.example.com/webhooks/nextcloud"
|
|
)
|
|
|
|
assert registered == [101, 102, 103]
|
|
assert client.create_webhook.await_count == len(preset["events"])
|
|
|
|
expected_auth = {"Authorization": "Bearer supersecret"}
|
|
for call, event_config in zip(
|
|
client.create_webhook.await_args_list, preset["events"]
|
|
):
|
|
kwargs = call.kwargs
|
|
assert kwargs["event"] == event_config["event"]
|
|
assert kwargs["uri"] == "https://mcp.example.com/webhooks/nextcloud"
|
|
assert kwargs["auth_method"] == "header"
|
|
assert kwargs["auth_data"] == expected_auth
|
|
# notes_sync uses path filters; ensure they round-trip through the helper
|
|
assert kwargs["event_filter"] == event_config["filter"]
|
|
|
|
|
|
async def test_register_uses_none_auth_when_secret_unset(monkeypatch, mocker):
|
|
_patch_secret(monkeypatch, None)
|
|
preset = get_preset("notes_sync")
|
|
assert preset is not None
|
|
client = _make_webhooks_client(mocker, ids=[1, 2, 3])
|
|
|
|
await _register_preset_webhooks(
|
|
client, preset, "https://mcp.example.com/webhooks/nextcloud"
|
|
)
|
|
|
|
for call in client.create_webhook.await_args_list:
|
|
assert call.kwargs["auth_method"] == "none"
|
|
assert call.kwargs["auth_data"] is None
|
|
|
|
|
|
async def test_register_returns_ids_in_call_order(monkeypatch, mocker):
|
|
_patch_secret(monkeypatch, None)
|
|
preset = get_preset("notes_sync")
|
|
assert preset is not None
|
|
client = _make_webhooks_client(mocker, ids=[42, 43, 44])
|
|
|
|
ids = await _register_preset_webhooks(client, preset, "https://example.com/wh")
|
|
|
|
assert ids == [42, 43, 44]
|