1b74b06753
Cut comment and docstring volume roughly in half across src, tests, examples, and docs_src: removed comments that restate the adjacent code, leftover development narration, section banners, and self-evident Args/Returns blocks, and compressed the remaining docstrings to a Google-style summary line plus only the detail that earns its place. Kept (and tightened) the load-bearing content: Raises sections, deprecation and version-availability notes, spec/RFC/issue references, why-comments for non-obvious decisions, and all coverage pragmas. The generated mcp_types.v* wire modules are untouched.
133 lines
4.5 KiB
Python
133 lines
4.5 KiB
Python
"""Tests for OAuth 2.0 shared code."""
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthMetadata
|
|
|
|
|
|
def test_oauth():
|
|
OAuthMetadata.model_validate(
|
|
{
|
|
"issuer": "https://example.com",
|
|
"authorization_endpoint": "https://example.com/oauth2/authorize",
|
|
"token_endpoint": "https://example.com/oauth2/token",
|
|
"scopes_supported": ["read", "write"],
|
|
"response_types_supported": ["code", "token"],
|
|
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
|
|
}
|
|
)
|
|
|
|
|
|
def test_oidc():
|
|
OAuthMetadata.model_validate(
|
|
{
|
|
"issuer": "https://example.com",
|
|
"authorization_endpoint": "https://example.com/oauth2/authorize",
|
|
"token_endpoint": "https://example.com/oauth2/token",
|
|
"end_session_endpoint": "https://example.com/logout",
|
|
"id_token_signing_alg_values_supported": ["RS256"],
|
|
"jwks_uri": "https://example.com/.well-known/jwks.json",
|
|
"response_types_supported": ["code", "token"],
|
|
"revocation_endpoint": "https://example.com/oauth2/revoke",
|
|
"scopes_supported": ["openid", "read", "write"],
|
|
"subject_types_supported": ["public"],
|
|
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
|
|
"userinfo_endpoint": "https://example.com/oauth2/userInfo",
|
|
}
|
|
)
|
|
|
|
|
|
def test_oauth_with_jarm():
|
|
OAuthMetadata.model_validate(
|
|
{
|
|
"issuer": "https://example.com",
|
|
"authorization_endpoint": "https://example.com/oauth2/authorize",
|
|
"token_endpoint": "https://example.com/oauth2/token",
|
|
"scopes_supported": ["read", "write"],
|
|
"response_types_supported": ["code", "token"],
|
|
"response_modes_supported": [
|
|
"query",
|
|
"fragment",
|
|
"form_post",
|
|
"query.jwt",
|
|
"fragment.jwt",
|
|
"form_post.jwt",
|
|
"jwt",
|
|
],
|
|
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
|
|
}
|
|
)
|
|
|
|
|
|
# RFC 7591 §2 marks client_uri/logo_uri/tos_uri/policy_uri/jwks_uri OPTIONAL, but some authorization
|
|
# servers echo a client's omitted metadata back as "" instead of dropping the keys. Without coercion,
|
|
# AnyHttpUrl rejects "" and the whole registration response fails despite its valid client_id.
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"empty_field",
|
|
["client_uri", "logo_uri", "tos_uri", "policy_uri", "jwks_uri"],
|
|
)
|
|
def test_optional_url_empty_string_coerced_to_none(empty_field: str):
|
|
data = {
|
|
"redirect_uris": ["https://example.com/callback"],
|
|
empty_field: "",
|
|
}
|
|
metadata = OAuthClientMetadata.model_validate(data)
|
|
assert getattr(metadata, empty_field) is None
|
|
|
|
|
|
def test_all_optional_urls_empty_together():
|
|
data = {
|
|
"redirect_uris": ["https://example.com/callback"],
|
|
"client_uri": "",
|
|
"logo_uri": "",
|
|
"tos_uri": "",
|
|
"policy_uri": "",
|
|
"jwks_uri": "",
|
|
}
|
|
metadata = OAuthClientMetadata.model_validate(data)
|
|
assert metadata.client_uri is None
|
|
assert metadata.logo_uri is None
|
|
assert metadata.tos_uri is None
|
|
assert metadata.policy_uri is None
|
|
assert metadata.jwks_uri is None
|
|
|
|
|
|
def test_valid_url_passes_through_unchanged():
|
|
data = {
|
|
"redirect_uris": ["https://example.com/callback"],
|
|
"client_uri": "https://udemy.com/",
|
|
}
|
|
metadata = OAuthClientMetadata.model_validate(data)
|
|
assert str(metadata.client_uri) == "https://udemy.com/"
|
|
|
|
|
|
def test_information_full_inherits_coercion():
|
|
data = {
|
|
"client_id": "abc123",
|
|
"redirect_uris": ["https://example.com/callback"],
|
|
"client_uri": "",
|
|
"logo_uri": "",
|
|
"tos_uri": "",
|
|
"policy_uri": "",
|
|
"jwks_uri": "",
|
|
}
|
|
info = OAuthClientInformationFull.model_validate(data)
|
|
assert info.client_id == "abc123"
|
|
assert info.client_uri is None
|
|
assert info.logo_uri is None
|
|
assert info.tos_uri is None
|
|
assert info.policy_uri is None
|
|
assert info.jwks_uri is None
|
|
|
|
|
|
def test_invalid_non_empty_url_still_rejected():
|
|
data = {
|
|
"redirect_uris": ["https://example.com/callback"],
|
|
"client_uri": "not a url",
|
|
}
|
|
with pytest.raises(ValidationError):
|
|
OAuthClientMetadata.model_validate(data)
|