@@ -2904,6 +2904,76 @@ async def test_mcd_init_with_non_string_domains_list():
29042904 ))
29052905
29062906
2907+ @pytest .mark .asyncio
2908+ async def test_mcd_init_client_id_requires_domain ():
2909+ """Test that client_id requires domain to be set (needed for token endpoint operations)."""
2910+ with pytest .raises (ConfigurationError , match = "'domain' parameter is required when 'client_id'" ):
2911+ ApiClient (ApiClientOptions (
2912+ domains = ["tenant.auth0.com" ],
2913+ client_id = "my-client" ,
2914+ client_secret = "my-secret" ,
2915+ audience = "my-audience"
2916+ ))
2917+
2918+ # Should work with both domain and domains
2919+ client = ApiClient (ApiClientOptions (
2920+ domain = "tenant.auth0.com" ,
2921+ domains = ["tenant.auth0.com" , "custom.example.com" ],
2922+ client_id = "my-client" ,
2923+ client_secret = "my-secret" ,
2924+ audience = "my-audience"
2925+ ))
2926+ assert client .options .client_id == "my-client"
2927+
2928+ # Should work with domains only (no client_id)
2929+ client = ApiClient (ApiClientOptions (
2930+ domains = ["tenant.auth0.com" ],
2931+ audience = "my-audience"
2932+ ))
2933+ assert client ._allowed_domains is not None
2934+
2935+
2936+ @pytest .mark .asyncio
2937+ async def test_cache_config_validation ():
2938+ """Test that cache_max_entries and cache_ttl_seconds are validated at init."""
2939+ with pytest .raises (ConfigurationError , match = "cache_ttl_seconds must be a non-negative number" ):
2940+ ApiClient (ApiClientOptions (
2941+ domain = "tenant.auth0.com" ,
2942+ audience = "my-audience" ,
2943+ cache_ttl_seconds = - 1
2944+ ))
2945+
2946+ with pytest .raises (ConfigurationError , match = "cache_max_entries must be an integer greater than 1" ):
2947+ ApiClient (ApiClientOptions (
2948+ domain = "tenant.auth0.com" ,
2949+ audience = "my-audience" ,
2950+ cache_max_entries = 0
2951+ ))
2952+
2953+ with pytest .raises (ConfigurationError , match = "cache_max_entries must be an integer greater than 1" ):
2954+ ApiClient (ApiClientOptions (
2955+ domain = "tenant.auth0.com" ,
2956+ audience = "my-audience" ,
2957+ cache_max_entries = 1
2958+ ))
2959+
2960+ with pytest .raises (ConfigurationError , match = "cache_max_entries must be an integer greater than 1" ):
2961+ ApiClient (ApiClientOptions (
2962+ domain = "tenant.auth0.com" ,
2963+ audience = "my-audience" ,
2964+ cache_max_entries = - 5
2965+ ))
2966+
2967+ # cache_ttl_seconds=0 is valid (always refetch), cache_max_entries=2 is minimum
2968+ client = ApiClient (ApiClientOptions (
2969+ domain = "tenant.auth0.com" ,
2970+ audience = "my-audience" ,
2971+ cache_ttl_seconds = 0 ,
2972+ cache_max_entries = 2
2973+ ))
2974+ assert client ._cache_ttl == 0
2975+
2976+
29072977@pytest .mark .asyncio
29082978async def test_mcd_resolve_allowed_domains_static_list ():
29092979 """Test _resolve_allowed_domains with static list."""
@@ -3111,6 +3181,21 @@ def bad_resolver(context):
31113181 await api_client ._resolve_allowed_domains ("https://tenant1.auth0.com/" )
31123182
31133183
3184+ @pytest .mark .asyncio
3185+ async def test_mcd_resolver_returns_invalid_domain_format ():
3186+ """Test that resolver returning domains with invalid format raises DomainsResolverError."""
3187+ def resolver_with_http (context ):
3188+ return ["tenant1.auth0.com" , "http://invalid.com" ]
3189+
3190+ api_client = ApiClient (ApiClientOptions (
3191+ domains = resolver_with_http ,
3192+ audience = "my-audience"
3193+ ))
3194+
3195+ with pytest .raises (DomainsResolverError , match = "Domains resolver returned invalid domain" ):
3196+ await api_client ._resolve_allowed_domains ("https://tenant1.auth0.com/" )
3197+
3198+
31143199@pytest .mark .asyncio
31153200async def test_mcd_verify_rejects_symmetric_algorithm ():
31163201 """Test that verify_access_token rejects tokens with symmetric algorithms (HS256)."""
@@ -3319,6 +3404,55 @@ async def test_mcd_second_issuer_validation(httpx_mock):
33193404 assert "verified token issuer does not match the discovery issuer" in str (err .value ).lower ()
33203405
33213406
3407+ @pytest .mark .asyncio
3408+ async def test_mcd_malformed_token_issuer_format (httpx_mock ):
3409+ """Test that a token with malformed iss claim raises VerifyAccessTokenError."""
3410+ # Generate token with http:// issuer (rejected by normalize_domain)
3411+ token = await generate_token (
3412+ domain = "evil.com" ,
3413+ user_id = "user123" ,
3414+ audience = "my-audience" ,
3415+ issuer = "http://evil.com"
3416+ )
3417+
3418+ api_client = ApiClient (ApiClientOptions (
3419+ domain = "tenant1.auth0.com" ,
3420+ audience = "my-audience"
3421+ ))
3422+
3423+ with pytest .raises (VerifyAccessTokenError , match = "Invalid token issuer format" ):
3424+ await api_client .verify_access_token (token )
3425+
3426+
3427+ @pytest .mark .asyncio
3428+ async def test_mcd_malformed_discovery_issuer_format (httpx_mock ):
3429+ """Test that malformed issuer in discovery metadata raises VerifyAccessTokenError."""
3430+ token = await generate_token (
3431+ domain = "tenant1.auth0.com" ,
3432+ user_id = "user123" ,
3433+ audience = "my-audience" ,
3434+ issuer = "https://tenant1.auth0.com/"
3435+ )
3436+
3437+ # Mock discovery returning malformed issuer with http://
3438+ httpx_mock .add_response (
3439+ method = "GET" ,
3440+ url = "https://tenant1.auth0.com/.well-known/openid-configuration" ,
3441+ json = {
3442+ "issuer" : "http://tenant1.auth0.com/" ,
3443+ "jwks_uri" : "https://tenant1.auth0.com/.well-known/jwks.json"
3444+ }
3445+ )
3446+
3447+ api_client = ApiClient (ApiClientOptions (
3448+ domain = "tenant1.auth0.com" ,
3449+ audience = "my-audience"
3450+ ))
3451+
3452+ with pytest .raises (VerifyAccessTokenError , match = "Invalid discovery issuer format" ):
3453+ await api_client .verify_access_token (token )
3454+
3455+
33223456@pytest .mark .asyncio
33233457async def test_mcd_discovery_missing_issuer_field (httpx_mock ):
33243458 """Test that missing issuer field in discovery causes clear error."""
0 commit comments