fix: address PR review feedback for client registry and DCR proxy

- Document wildcard scope policy in ClientRegistry class docstring
- Add hostname None guard and IPv6 loopback (::1) to redirect URI validation
- Simplify redirect URI scheme validation into single guard clause
- Add try/finally cleanup to DCR client deletion test
- Validate 302 Location header in unknown client rejection test
- Add unit tests for IPv6 loopback, malformed URIs, and DCR proxy paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-04-05 19:29:23 +02:00
co-authored by Claude Opus 4.6
parent 7d775d2a52
commit b07b713146
4 changed files with 144 additions and 14 deletions
+33
View File
@@ -165,3 +165,36 @@ def test_client_name_resolution(monkeypatch):
registry = _get_registry(monkeypatch, "claude-desktop, custom-tool")
assert registry.get_client("claude-desktop").name == "Claude Desktop"
assert registry.get_client("custom-tool").name == "Custom Tool"
def test_ipv6_loopback_allowed(monkeypatch):
registry = _get_registry(monkeypatch, "ipv6-app|http://[::1]:3000/cb")
client = registry.get_client("ipv6-app")
assert client is not None
assert client.redirect_uris == ["http://[::1]:3000/cb"]
def test_malformed_uri_no_hostname_skipped(monkeypatch, caplog):
with caplog.at_level(logging.WARNING):
registry = _get_registry(monkeypatch, "bad|http:///no-host")
assert registry.get_client("bad") is None
assert "cannot parse hostname" in caplog.text
def test_validate_redirect_uri_ipv6_loopback(monkeypatch):
"""IPv6 loopback redirect URIs should match wildcard localhost patterns."""
registry = _get_registry(monkeypatch, "ipv6-app|http://[::1]:3000/cb")
valid, err = registry.validate_client(
"ipv6-app", redirect_uri="http://[::1]:3000/cb"
)
assert valid is True
assert err is None
def test_validate_redirect_uri_no_hostname(monkeypatch):
"""Redirect URIs with no parseable hostname should be rejected."""
registry = _get_registry(monkeypatch, "test-client")
valid, err = registry.validate_client("test-client", redirect_uri="not-a-uri")
assert valid is False
assert "redirect_uri" in err.lower()
+65
View File
@@ -0,0 +1,65 @@
"""Unit tests for DCR proxy registration_not_supported path."""
import json
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from nextcloud_mcp_server.auth.oauth_routes import oauth_register_proxy
pytestmark = pytest.mark.unit
def _make_request(body: dict, oauth_config: dict) -> MagicMock:
"""Create a mock Starlette Request."""
request = AsyncMock()
request.json = AsyncMock(return_value=body)
request.client = MagicMock()
request.client.host = "127.0.0.1"
request.app = MagicMock()
request.app.state.oauth_context = {"config": oauth_config}
return request
_DCR_BODY = {
"client_name": "test",
"redirect_uris": ["http://localhost:9999/cb"],
}
async def test_registration_not_supported_when_no_endpoint():
"""When discovery doc lacks registration_endpoint, return 400."""
request = _make_request(
body=_DCR_BODY,
oauth_config={
"discovery_url": "https://idp.example.com/.well-known/openid-configuration"
},
)
discovery_doc = {
"issuer": "https://idp.example.com",
"authorization_endpoint": "https://idp.example.com/auth",
}
with patch(
"nextcloud_mcp_server.auth.oauth_routes._get_cached_discovery",
new_callable=AsyncMock,
return_value=discovery_doc,
):
response = await oauth_register_proxy(request)
assert response.status_code == 400
body = json.loads(response.body)
assert body["error"] == "registration_not_supported"
assert "ALLOWED_MCP_CLIENTS" in body["error_description"]
async def test_registration_not_supported_when_no_discovery_url():
"""When no discovery_url is configured, return 400."""
request = _make_request(body=_DCR_BODY, oauth_config={})
response = await oauth_register_proxy(request)
assert response.status_code == 400
body = json.loads(response.body)
assert body["error"] == "registration_not_supported"