|
1 | 1 | """Tests for OAuth 2.0 shared code.""" |
2 | 2 |
|
3 | 3 | import pytest |
4 | | -from pydantic import AnyUrl, ValidationError |
| 4 | +from pydantic import AnyHttpUrl, AnyUrl, ValidationError, create_model |
5 | 5 |
|
6 | 6 | from mcp.shared.auth import InvalidRedirectUriError, OAuthClientInformationFull, OAuthClientMetadata, OAuthMetadata |
7 | 7 |
|
@@ -218,6 +218,51 @@ def test_client_with_no_registered_redirect_uris_cannot_resolve_a_redirect( |
218 | 218 | info.validate_redirect_uri(redirect_uri) |
219 | 219 |
|
220 | 220 |
|
| 221 | +@pytest.mark.parametrize("model", [OAuthClientMetadata, OAuthClientInformationFull]) |
| 222 | +@pytest.mark.parametrize("container", [list, tuple]) |
| 223 | +def test_redirect_uri_subtypes_are_stored_as_any_url( |
| 224 | + model: type[OAuthClientMetadata] | type[OAuthClientInformationFull], |
| 225 | + container: type[list[AnyHttpUrl]] | type[tuple[AnyHttpUrl, ...]], |
| 226 | +) -> None: |
| 227 | + """SDK-defined registration models compare a URL subclass with the incoming base URL.""" |
| 228 | + url = "https://example.com/callback" |
| 229 | + data: dict[str, object] = {"redirect_uris": container([AnyHttpUrl(url)])} |
| 230 | + if model is OAuthClientInformationFull: |
| 231 | + data["client_id"] = "abc123" |
| 232 | + info = model.model_validate(data, strict=container is list) |
| 233 | + |
| 234 | + assert info.redirect_uris == [AnyUrl(url)] |
| 235 | + assert info.model_dump(mode="json")["redirect_uris"] == [url] |
| 236 | + if isinstance(info, OAuthClientInformationFull): |
| 237 | + assert info.validate_redirect_uri(AnyUrl(url)) == AnyUrl(url) |
| 238 | + with pytest.raises(InvalidRedirectUriError): |
| 239 | + info.validate_redirect_uri(AnyUrl("https://example.com/other")) |
| 240 | + |
| 241 | + |
| 242 | +@pytest.mark.parametrize("model", [OAuthClientMetadata, OAuthClientInformationFull]) |
| 243 | +def test_redirect_uri_normalization_preserves_strict_list_validation( |
| 244 | + model: type[OAuthClientMetadata] | type[OAuthClientInformationFull], |
| 245 | +) -> None: |
| 246 | + """SDK-defined strict parsing still rejects non-list redirect URI containers.""" |
| 247 | + data: dict[str, object] = {"redirect_uris": (AnyHttpUrl("https://example.com/callback"),)} |
| 248 | + if model is OAuthClientInformationFull: |
| 249 | + data["client_id"] = "abc123" |
| 250 | + |
| 251 | + with pytest.raises(ValidationError): |
| 252 | + model.model_validate(data, strict=True) |
| 253 | + |
| 254 | + |
| 255 | +def test_redeclared_redirect_uri_field_keeps_its_own_validation() -> None: |
| 256 | + """SDK-defined URL normalization does not override a subclass's replacement field.""" |
| 257 | + |
| 258 | + CustomClientInformation = create_model( |
| 259 | + "CustomClientInformation", __base__=OAuthClientInformationFull, redirect_uris=(list[str] | None, None) |
| 260 | + ) |
| 261 | + info = CustomClientInformation.model_validate({"client_id": "abc123", "redirect_uris": ["custom redirect"]}) |
| 262 | + |
| 263 | + assert info.redirect_uris == ["custom redirect"] |
| 264 | + |
| 265 | + |
221 | 266 | def test_request_metadata_restricts_application_type_to_the_values_the_sdk_sends(): |
222 | 267 | """What the SDK sends stays narrow even though what it accepts back is wide.""" |
223 | 268 | with pytest.raises(ValidationError): |
|
0 commit comments