Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

164 lines
5.9 KiB
Python

"""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