# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from pathlib import Path import subprocess import sys from fastapi.openapi.models import OAuth2 from fastapi.openapi.models import OAuthFlowAuthorizationCode from fastapi.openapi.models import OAuthFlows from google.adk.auth.auth_credential import AuthCredential from google.adk.auth.auth_credential import AuthCredentialTypes from google.adk.auth.auth_credential import OAuth2Auth from google.adk.auth.auth_schemes import CustomAuthScheme from google.adk.auth.auth_tool import AuthConfig import pytest class TestAuthConfig: """Tests for the AuthConfig method.""" @pytest.fixture def oauth2_auth_scheme(): """Create an OAuth2 auth scheme for testing.""" # Create the OAuthFlows object first flows = OAuthFlows( authorizationCode=OAuthFlowAuthorizationCode( authorizationUrl="https://example.com/oauth2/authorize", tokenUrl="https://example.com/oauth2/token", scopes={"read": "Read access", "write": "Write access"}, ) ) # Then create the OAuth2 object with the flows return OAuth2(flows=flows) @pytest.fixture def oauth2_credentials(): """Create OAuth2 credentials for testing.""" return AuthCredential( auth_type=AuthCredentialTypes.OAUTH2, oauth2=OAuth2Auth( client_id="mock_client_id", client_secret="mock_client_secret", redirect_uri="https://example.com/callback", ), ) @pytest.fixture def auth_config(oauth2_auth_scheme, oauth2_credentials): """Create an AuthConfig for testing.""" # Create a copy of the credentials for the exchanged_auth_credential exchanged_credential = oauth2_credentials.model_copy(deep=True) return AuthConfig( auth_scheme=oauth2_auth_scheme, raw_auth_credential=oauth2_credentials, exchanged_auth_credential=exchanged_credential, ) @pytest.fixture def auth_config_with_key(oauth2_auth_scheme, oauth2_credentials): """Create an AuthConfig for testing.""" return AuthConfig( auth_scheme=oauth2_auth_scheme, raw_auth_credential=oauth2_credentials, credential_key="test_key", ) def test_custom_credential_key(auth_config_with_key): """Test using custom credential key.""" key = auth_config_with_key.credential_key assert key == "test_key" def test_credential_key(auth_config): """Test generating a unique credential key.""" key = auth_config.credential_key assert key.startswith("adk_oauth2_") assert "_oauth2_" in key def test_get_credential_key_with_extras(auth_config): """Test generating a key when model_extra exists.""" # Add model_extra to test cleanup original_key = auth_config.credential_key key = auth_config.credential_key auth_config.auth_scheme.model_extra["extra_field"] = "value" auth_config.raw_auth_credential.model_extra["extra_field"] = "value" assert original_key == key assert "extra_field" in auth_config.auth_scheme.model_extra assert "extra_field" in auth_config.raw_auth_credential.model_extra def test_credential_key_is_stable_across_python_hash_seed(): """Test AuthConfig key generation does not depend on PYTHONHASHSEED.""" repo_root = Path(__file__).resolve().parents[3] pythonpath = str(repo_root / "src") code = "\n".join([ "from fastapi.openapi.models import OAuth2", "from fastapi.openapi.models import OAuthFlowAuthorizationCode", "from fastapi.openapi.models import OAuthFlows", "from google.adk.auth.auth_credential import AuthCredential", "from google.adk.auth.auth_credential import AuthCredentialTypes", "from google.adk.auth.auth_credential import OAuth2Auth", "from google.adk.auth.auth_tool import AuthConfig", "", "auth_scheme = OAuth2(", " flows=OAuthFlows(", " authorizationCode=OAuthFlowAuthorizationCode(", " authorizationUrl='https://example.com/oauth2/authorize',", " tokenUrl='https://example.com/oauth2/token',", " scopes={'read': 'Read access'},", " )", " )", ")", "auth_cred = AuthCredential(", " auth_type=AuthCredentialTypes.OAUTH2,", " oauth2=OAuth2Auth(", " client_id='mock_client_id',", " client_secret='mock_client_secret',", " ),", ")", "print(AuthConfig(", " auth_scheme=auth_scheme,", " raw_auth_credential=auth_cred,", ").credential_key)", ]) def _run_with_seed(seed: str) -> str: env = os.environ.copy() env["PYTHONHASHSEED"] = seed env["PYTHONPATH"] = os.pathsep.join( [pythonpath, env.get("PYTHONPATH", "")] ).strip(os.pathsep) return subprocess.check_output( [sys.executable, "-c", code], env=env, text=True, ).strip() assert _run_with_seed("0") == _run_with_seed("1") def test_credential_key_with_custom_auth_scheme(): """Test generating a credential key when the auth scheme is a CustomAuthScheme (type_ is a string).""" custom_scheme = CustomAuthScheme.model_validate({"type": "mock_custom_type"}) custom_config = AuthConfig( auth_scheme=custom_scheme, ) key = custom_config.credential_key assert key.startswith("adk_mock_custom_type_") assert len(key) > len("adk_mock_custom_type_") def test_credential_key_is_stable_across_redirect_uri(oauth2_auth_scheme): """AuthConfig.credential_key should be invariant under redirect_uri changes. redirect_uri is deployment configuration (which callback URL the auth server should redirect to), not part of the credential identity. Two AuthConfig instances built from credentials that share the same client_id, client_secret, and scopes but differ only in redirect_uri should produce the same credential_key. """ credential_local = AuthCredential( auth_type=AuthCredentialTypes.OAUTH2, oauth2=OAuth2Auth( client_id="client", client_secret="secret", redirect_uri="http://localhost:8001/oauth2callback", ), ) credential_deployed = AuthCredential( auth_type=AuthCredentialTypes.OAUTH2, oauth2=OAuth2Auth( client_id="client", client_secret="secret", redirect_uri="https://deployed.example.com/oauth2callback", ), ) config_local = AuthConfig( auth_scheme=oauth2_auth_scheme, raw_auth_credential=credential_local, ) config_deployed = AuthConfig( auth_scheme=oauth2_auth_scheme, raw_auth_credential=credential_deployed, ) assert config_local.credential_key == config_deployed.credential_key