Skip to content

Commit b0c83a8

Browse files
test: move OAuthClientMetadata empty-URL tests to tests/shared, drop Test class
1 parent fef29a5 commit b0c83a8

File tree

2 files changed

+82
-75
lines changed

2 files changed

+82
-75
lines changed

tests/client/test_auth.py

Lines changed: 1 addition & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import httpx
99
import pytest
1010
from inline_snapshot import Is, snapshot
11-
from pydantic import AnyHttpUrl, AnyUrl, ValidationError
11+
from pydantic import AnyHttpUrl, AnyUrl
1212

1313
from mcp.client.auth import OAuthClientProvider, PKCEParameters
1414
from mcp.client.auth.exceptions import OAuthFlowError
@@ -985,79 +985,6 @@ def text(self):
985985
assert "Registration failed: 400" in str(exc_info.value)
986986

987987

988-
class TestOAuthClientMetadataEmptyUrlCoercion:
989-
"""RFC 7591 §2 marks client_uri/logo_uri/tos_uri/policy_uri/jwks_uri as OPTIONAL.
990-
Some authorization servers echo the client's omitted metadata back as ""
991-
instead of dropping the keys; without coercion, AnyHttpUrl rejects "" and
992-
the whole registration response is thrown away even though the server
993-
returned a valid client_id."""
994-
995-
@pytest.mark.parametrize(
996-
"empty_field",
997-
["client_uri", "logo_uri", "tos_uri", "policy_uri", "jwks_uri"],
998-
)
999-
def test_optional_url_empty_string_coerced_to_none(self, empty_field: str):
1000-
data = {
1001-
"redirect_uris": ["https://example.com/callback"],
1002-
empty_field: "",
1003-
}
1004-
metadata = OAuthClientMetadata.model_validate(data)
1005-
assert getattr(metadata, empty_field) is None
1006-
1007-
def test_all_optional_urls_empty_together(self):
1008-
data = {
1009-
"redirect_uris": ["https://example.com/callback"],
1010-
"client_uri": "",
1011-
"logo_uri": "",
1012-
"tos_uri": "",
1013-
"policy_uri": "",
1014-
"jwks_uri": "",
1015-
}
1016-
metadata = OAuthClientMetadata.model_validate(data)
1017-
assert metadata.client_uri is None
1018-
assert metadata.logo_uri is None
1019-
assert metadata.tos_uri is None
1020-
assert metadata.policy_uri is None
1021-
assert metadata.jwks_uri is None
1022-
1023-
def test_valid_url_passes_through_unchanged(self):
1024-
data = {
1025-
"redirect_uris": ["https://example.com/callback"],
1026-
"client_uri": "https://udemy.com/",
1027-
}
1028-
metadata = OAuthClientMetadata.model_validate(data)
1029-
assert str(metadata.client_uri) == "https://udemy.com/"
1030-
1031-
def test_information_full_inherits_coercion(self):
1032-
"""OAuthClientInformationFull subclasses OAuthClientMetadata, so the
1033-
same coercion applies to DCR responses parsed via the full model."""
1034-
data = {
1035-
"client_id": "abc123",
1036-
"redirect_uris": ["https://example.com/callback"],
1037-
"client_uri": "",
1038-
"logo_uri": "",
1039-
"tos_uri": "",
1040-
"policy_uri": "",
1041-
"jwks_uri": "",
1042-
}
1043-
info = OAuthClientInformationFull.model_validate(data)
1044-
assert info.client_id == "abc123"
1045-
assert info.client_uri is None
1046-
assert info.logo_uri is None
1047-
assert info.tos_uri is None
1048-
assert info.policy_uri is None
1049-
assert info.jwks_uri is None
1050-
1051-
def test_invalid_non_empty_url_still_rejected(self):
1052-
"""Coercion must only touch empty strings — garbage URLs still raise."""
1053-
data = {
1054-
"redirect_uris": ["https://example.com/callback"],
1055-
"client_uri": "not a url",
1056-
}
1057-
with pytest.raises(ValidationError):
1058-
OAuthClientMetadata.model_validate(data)
1059-
1060-
1061988
class TestCreateClientRegistrationRequest:
1062989
"""Test client registration request creation."""
1063990

tests/shared/test_auth.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
"""Tests for OAuth 2.0 shared code."""
22

3-
from mcp.shared.auth import OAuthMetadata
3+
import pytest
4+
from pydantic import ValidationError
5+
6+
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthMetadata
47

58

69
def test_oauth():
@@ -58,3 +61,80 @@ def test_oauth_with_jarm():
5861
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
5962
}
6063
)
64+
65+
66+
# RFC 7591 §2 marks client_uri/logo_uri/tos_uri/policy_uri/jwks_uri as OPTIONAL.
67+
# Some authorization servers echo the client's omitted metadata back as ""
68+
# instead of dropping the keys; without coercion, AnyHttpUrl rejects "" and
69+
# the whole registration response is thrown away even though the server
70+
# returned a valid client_id.
71+
72+
73+
@pytest.mark.parametrize(
74+
"empty_field",
75+
["client_uri", "logo_uri", "tos_uri", "policy_uri", "jwks_uri"],
76+
)
77+
def test_optional_url_empty_string_coerced_to_none(empty_field: str):
78+
data = {
79+
"redirect_uris": ["https://example.com/callback"],
80+
empty_field: "",
81+
}
82+
metadata = OAuthClientMetadata.model_validate(data)
83+
assert getattr(metadata, empty_field) is None
84+
85+
86+
def test_all_optional_urls_empty_together():
87+
data = {
88+
"redirect_uris": ["https://example.com/callback"],
89+
"client_uri": "",
90+
"logo_uri": "",
91+
"tos_uri": "",
92+
"policy_uri": "",
93+
"jwks_uri": "",
94+
}
95+
metadata = OAuthClientMetadata.model_validate(data)
96+
assert metadata.client_uri is None
97+
assert metadata.logo_uri is None
98+
assert metadata.tos_uri is None
99+
assert metadata.policy_uri is None
100+
assert metadata.jwks_uri is None
101+
102+
103+
def test_valid_url_passes_through_unchanged():
104+
data = {
105+
"redirect_uris": ["https://example.com/callback"],
106+
"client_uri": "https://udemy.com/",
107+
}
108+
metadata = OAuthClientMetadata.model_validate(data)
109+
assert str(metadata.client_uri) == "https://udemy.com/"
110+
111+
112+
def test_information_full_inherits_coercion():
113+
"""OAuthClientInformationFull subclasses OAuthClientMetadata, so the
114+
same coercion applies to DCR responses parsed via the full model."""
115+
data = {
116+
"client_id": "abc123",
117+
"redirect_uris": ["https://example.com/callback"],
118+
"client_uri": "",
119+
"logo_uri": "",
120+
"tos_uri": "",
121+
"policy_uri": "",
122+
"jwks_uri": "",
123+
}
124+
info = OAuthClientInformationFull.model_validate(data)
125+
assert info.client_id == "abc123"
126+
assert info.client_uri is None
127+
assert info.logo_uri is None
128+
assert info.tos_uri is None
129+
assert info.policy_uri is None
130+
assert info.jwks_uri is None
131+
132+
133+
def test_invalid_non_empty_url_still_rejected():
134+
"""Coercion must only touch empty strings — garbage URLs still raise."""
135+
data = {
136+
"redirect_uris": ["https://example.com/callback"],
137+
"client_uri": "not a url",
138+
}
139+
with pytest.raises(ValidationError):
140+
OAuthClientMetadata.model_validate(data)

0 commit comments

Comments
 (0)