"""Tests for the API-key helpers on BaseLLMProvider. Covers ``resolve_api_key`` (None vs raise vs string), the ``resolve_api_key_or_placeholder`` companion that always returns a string, ``build_bearer_header``, and the ``has_api_key`` predicate. """ from unittest.mock import patch import pytest from local_deep_research.llm.providers.base import ( BaseLLMProvider, OPTIONAL_API_KEY_PLACEHOLDER, ) class _RequiredProvider(BaseLLMProvider): """Test fixture: a provider that requires an API key.""" provider_name = "RequiredProvider" api_key_setting = "test.required.api_key" api_key_optional = False class _OptionalProvider(BaseLLMProvider): """Test fixture: a provider that allows missing API key.""" provider_name = "OptionalProvider" api_key_setting = "test.optional.api_key" api_key_optional = True class _NoSettingProvider(BaseLLMProvider): """Test fixture: a provider with no api_key_setting at all.""" provider_name = "NoSettingProvider" api_key_setting = None SETTINGS = ( "local_deep_research.config.thread_settings.get_setting_from_snapshot" ) def _set(value): """Patch get_setting_from_snapshot to return ``value`` for any key.""" def side_effect(key, default=None, *args, **kwargs): if key == _RequiredProvider.api_key_setting: return value if key == _OptionalProvider.api_key_setting: return value return default return side_effect class TestResolveApiKey: def test_required_with_real_key_returns_stripped(self): with patch(SETTINGS, side_effect=_set(" sk-key ")): assert _RequiredProvider.resolve_api_key() == "sk-key" def test_required_with_missing_key_raises(self): with patch(SETTINGS, side_effect=_set(None)): with pytest.raises(ValueError, match="API key not configured"): _RequiredProvider.resolve_api_key() def test_required_with_empty_string_raises(self): with patch(SETTINGS, side_effect=_set("")): with pytest.raises(ValueError): _RequiredProvider.resolve_api_key() def test_required_with_whitespace_only_raises(self): with patch(SETTINGS, side_effect=_set(" ")): with pytest.raises(ValueError): _RequiredProvider.resolve_api_key() def test_optional_with_real_key_returns_stripped(self): with patch(SETTINGS, side_effect=_set(" sk-opt ")): assert _OptionalProvider.resolve_api_key() == "sk-opt" def test_optional_with_missing_key_returns_none(self): with patch(SETTINGS, side_effect=_set(None)): assert _OptionalProvider.resolve_api_key() is None def test_optional_with_whitespace_only_returns_none(self): with patch(SETTINGS, side_effect=_set("\t \n")): assert _OptionalProvider.resolve_api_key() is None def test_no_setting_returns_none(self): # Patching is unnecessary — api_key_setting is None so no read happens assert _NoSettingProvider.resolve_api_key() is None class TestResolveApiKeyOrPlaceholder: def test_returns_real_key_when_set(self): with patch(SETTINGS, side_effect=_set("real")): assert _OptionalProvider.resolve_api_key_or_placeholder() == "real" def test_returns_placeholder_when_optional_and_missing(self): with patch(SETTINGS, side_effect=_set("")): assert ( _OptionalProvider.resolve_api_key_or_placeholder() == OPTIONAL_API_KEY_PLACEHOLDER ) def test_required_missing_still_raises(self): # The "_or_placeholder" variant only swallows missing for OPTIONAL # providers; required providers must still raise so cloud-API # callers get a clear error instead of silently sending a fake key. with patch(SETTINGS, side_effect=_set("")): with pytest.raises(ValueError): _RequiredProvider.resolve_api_key_or_placeholder() class TestBuildBearerHeader: def test_returns_empty_when_no_key(self): with patch(SETTINGS, side_effect=_set(None)): assert _OptionalProvider.build_bearer_header() == {} def test_returns_empty_for_whitespace_only(self): with patch(SETTINGS, side_effect=_set(" ")): assert _OptionalProvider.build_bearer_header() == {} def test_returns_bearer_when_key_set(self): with patch(SETTINGS, side_effect=_set("abc")): assert _OptionalProvider.build_bearer_header() == { "Authorization": "Bearer abc" } def test_strips_whitespace_in_header_value(self): with patch(SETTINGS, side_effect=_set(" abc ")): assert _OptionalProvider.build_bearer_header() == { "Authorization": "Bearer abc" } def test_required_provider_with_no_key_returns_empty(self): # ValueError from resolve_api_key is swallowed — caller can decide # what to do (e.g., is_available returns False elsewhere). with patch(SETTINGS, side_effect=_set(None)): assert _RequiredProvider.build_bearer_header() == {} class TestHasApiKey: def test_true_when_real_key_set(self): with patch(SETTINGS, side_effect=_set("real")): assert _RequiredProvider.has_api_key() is True def test_false_when_required_and_missing(self): with patch(SETTINGS, side_effect=_set(None)): assert _RequiredProvider.has_api_key() is False def test_false_when_optional_and_missing(self): with patch(SETTINGS, side_effect=_set("")): assert _OptionalProvider.has_api_key() is False def test_false_when_settings_raise_unexpectedly(self): # Defense-in-depth: settings infrastructure failures should not # propagate from is_available()-style callers. with patch(SETTINGS, side_effect=RuntimeError("settings down")): assert _RequiredProvider.has_api_key() is False assert _OptionalProvider.has_api_key() is False