from types import MappingProxyType import pytest from pydantic import SecretStr, ValidationError from openhands.events.action.commands import CmdRunAction from openhands.integrations.provider import ( ProviderHandler, ProviderToken, ProviderType, ) from openhands.storage.data_models.settings import Settings from openhands.storage.data_models.user_secrets import UserSecrets def test_provider_token_immutability(): """Test that ProviderToken is immutable""" token = ProviderToken(token=SecretStr('test'), user_id='user1') # Test direct attribute modification with pytest.raises(ValidationError): token.token = SecretStr('new') with pytest.raises(ValidationError): token.user_id = 'new_user' # Test that __setattr__ is blocked with pytest.raises(ValidationError): setattr(token, 'token', SecretStr('new')) # Verify original values are unchanged assert token.token.get_secret_value() == 'test' assert token.user_id == 'user1' def test_secret_store_immutability(): """Test that UserSecrets is immutable""" store = UserSecrets( provider_tokens={ProviderType.GITHUB: ProviderToken(token=SecretStr('test'))} ) # Test direct attribute modification with pytest.raises(ValidationError): store.provider_tokens = {} # Test dictionary mutation attempts with pytest.raises((TypeError, AttributeError)): store.provider_tokens[ProviderType.GITHUB] = ProviderToken( token=SecretStr('new') ) with pytest.raises((TypeError, AttributeError)): store.provider_tokens.clear() with pytest.raises((TypeError, AttributeError)): store.provider_tokens.update( {ProviderType.GITLAB: ProviderToken(token=SecretStr('test'))} ) # Test nested immutability github_token = store.provider_tokens[ProviderType.GITHUB] with pytest.raises(ValidationError): github_token.token = SecretStr('new') # Verify original values are unchanged assert store.provider_tokens[ProviderType.GITHUB].token.get_secret_value() == 'test' def test_settings_immutability(): """Test that Settings secrets_store is immutable""" settings = Settings( secrets_store=UserSecrets( provider_tokens={ ProviderType.GITHUB: ProviderToken(token=SecretStr('test')) } ) ) # Test direct modification of secrets_store with pytest.raises(ValidationError): settings.secrets_store = UserSecrets() # Test nested modification attempts with pytest.raises((TypeError, AttributeError)): settings.secrets_store.provider_tokens[ProviderType.GITHUB] = ProviderToken( token=SecretStr('new') ) # Test model_copy creates new instance new_store = UserSecrets( provider_tokens={ ProviderType.GITHUB: ProviderToken(token=SecretStr('new_token')) } ) new_settings = settings.model_copy(update={'secrets_store': new_store}) # Verify original is unchanged and new has updated values assert ( settings.secrets_store.provider_tokens[ ProviderType.GITHUB ].token.get_secret_value() == 'test' ) assert ( new_settings.secrets_store.provider_tokens[ ProviderType.GITHUB ].token.get_secret_value() == 'new_token' ) with pytest.raises(ValidationError): new_settings.secrets_store.provider_tokens[ ProviderType.GITHUB ].token = SecretStr('') def test_provider_handler_immutability(): """Test that ProviderHandler maintains token immutability""" # Create initial tokens tokens = MappingProxyType( {ProviderType.GITHUB: ProviderToken(token=SecretStr('test'))} ) handler = ProviderHandler(provider_tokens=tokens) # Try to modify tokens (should raise TypeError due to frozen dict) with pytest.raises((TypeError, AttributeError)): handler.provider_tokens[ProviderType.GITHUB] = ProviderToken( token=SecretStr('new') ) # Try to modify the handler's tokens property with pytest.raises((ValidationError, TypeError, AttributeError)): handler.provider_tokens = {} # Original token should be unchanged assert ( handler.provider_tokens[ProviderType.GITHUB].token.get_secret_value() == 'test' ) def test_token_conversion(): """Test token conversion in UserSecrets.create""" # Test with string token store1 = Settings( secrets_store=UserSecrets( provider_tokens={ ProviderType.GITHUB: ProviderToken(token=SecretStr('test_token')) } ) ) assert ( store1.secrets_store.provider_tokens[ ProviderType.GITHUB ].token.get_secret_value() == 'test_token' ) assert store1.secrets_store.provider_tokens[ProviderType.GITHUB].user_id is None # Test with dict token store2 = UserSecrets( provider_tokens={'github': {'token': 'test_token', 'user_id': 'user1'}} ) assert ( store2.provider_tokens[ProviderType.GITHUB].token.get_secret_value() == 'test_token' ) assert store2.provider_tokens[ProviderType.GITHUB].user_id == 'user1' # Test with ProviderToken token = ProviderToken(token=SecretStr('test_token'), user_id='user2') store3 = UserSecrets(provider_tokens={ProviderType.GITHUB: token}) assert ( store3.provider_tokens[ProviderType.GITHUB].token.get_secret_value() == 'test_token' ) assert store3.provider_tokens[ProviderType.GITHUB].user_id == 'user2' store4 = UserSecrets( provider_tokens={ ProviderType.GITHUB: 123 # Invalid type } ) assert ProviderType.GITHUB not in store4.provider_tokens # Test with empty/None token store5 = UserSecrets(provider_tokens={ProviderType.GITHUB: None}) assert ProviderType.GITHUB not in store5.provider_tokens store6 = UserSecrets( provider_tokens={ 'invalid_provider': 'test_token' # Invalid provider type } ) assert len(store6.provider_tokens.keys()) == 0 def test_provider_handler_type_enforcement(): with pytest.raises((TypeError)): ProviderHandler(provider_tokens={'a': 'b'}) def test_expose_env_vars(): """Test that expose_env_vars correctly exposes secrets as strings""" tokens = MappingProxyType( { ProviderType.GITHUB: ProviderToken(token=SecretStr('test_token')), ProviderType.GITLAB: ProviderToken(token=SecretStr('gitlab_token')), } ) handler = ProviderHandler(provider_tokens=tokens) # Test with specific provider tokens env_secrets = { ProviderType.GITHUB: SecretStr('gh_token'), ProviderType.GITLAB: SecretStr('gl_token'), } exposed = handler.expose_env_vars(env_secrets) assert exposed['github_token'] == 'gh_token' assert exposed['gitlab_token'] == 'gl_token' @pytest.mark.asyncio async def test_get_env_vars(): """Test get_env_vars with different configurations""" tokens = MappingProxyType( { ProviderType.GITHUB: ProviderToken(token=SecretStr('test_token')), ProviderType.GITLAB: ProviderToken(token=SecretStr('gitlab_token')), } ) handler = ProviderHandler(provider_tokens=tokens) # Test getting all tokens unexposed env_vars = await handler.get_env_vars(expose_secrets=False) assert isinstance(env_vars, dict) assert isinstance(env_vars[ProviderType.GITHUB], SecretStr) assert env_vars[ProviderType.GITHUB].get_secret_value() == 'test_token' assert env_vars[ProviderType.GITLAB].get_secret_value() == 'gitlab_token' # Test getting specific providers env_vars = await handler.get_env_vars( expose_secrets=False, providers=[ProviderType.GITHUB] ) assert len(env_vars) == 1 assert ProviderType.GITHUB in env_vars assert ProviderType.GITLAB not in env_vars # Test exposed secrets exposed_vars = await handler.get_env_vars(expose_secrets=True) assert isinstance(exposed_vars, dict) assert exposed_vars['github_token'] == 'test_token' assert exposed_vars['gitlab_token'] == 'gitlab_token' # Test empty tokens empty_handler = ProviderHandler(provider_tokens=MappingProxyType({})) empty_vars = await empty_handler.get_env_vars() assert empty_vars == {} @pytest.fixture def event_stream(): """Fixture for event stream testing""" class TestEventStream: def __init__(self): self.secrets = {} def set_secrets(self, secrets): self.secrets = secrets return TestEventStream() @pytest.mark.asyncio async def test_set_event_stream_secrets(event_stream): """Test setting secrets in event stream""" tokens = MappingProxyType( { ProviderType.GITHUB: ProviderToken(token=SecretStr('test_token')), ProviderType.GITLAB: ProviderToken(token=SecretStr('gitlab_token')), } ) handler = ProviderHandler(provider_tokens=tokens) # Test with provided env_vars env_vars = { ProviderType.GITHUB: SecretStr('new_token'), ProviderType.GITLAB: SecretStr('new_gitlab_token'), } await handler.set_event_stream_secrets(event_stream, env_vars) assert event_stream.secrets == { 'github_token': 'new_token', 'gitlab_token': 'new_gitlab_token', } # Test without env_vars (using existing tokens) await handler.set_event_stream_secrets(event_stream) assert event_stream.secrets == { 'github_token': 'test_token', 'gitlab_token': 'gitlab_token', } def test_check_cmd_action_for_provider_token_ref(): """Test detection of provider tokens in command actions""" # Test command with GitHub token cmd = CmdRunAction(command='echo $GITHUB_TOKEN') providers = ProviderHandler.check_cmd_action_for_provider_token_ref(cmd) assert ProviderType.GITHUB in providers assert len(providers) == 1 # Test command with multiple tokens cmd = CmdRunAction(command='echo $GITHUB_TOKEN && echo $GITLAB_TOKEN') providers = ProviderHandler.check_cmd_action_for_provider_token_ref(cmd) assert ProviderType.GITHUB in providers assert ProviderType.GITLAB in providers assert len(providers) == 2 # Test command without tokens cmd = CmdRunAction(command='echo "Hello"') providers = ProviderHandler.check_cmd_action_for_provider_token_ref(cmd) assert len(providers) == 0 # Test non-command action from openhands.events.action import MessageAction msg = MessageAction(content='test') providers = ProviderHandler.check_cmd_action_for_provider_token_ref(msg) assert len(providers) == 0 def test_get_provider_env_key(): """Test provider environment key generation""" assert ProviderHandler.get_provider_env_key(ProviderType.GITHUB) == 'github_token' assert ProviderHandler.get_provider_env_key(ProviderType.GITLAB) == 'gitlab_token'