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
645 lines
23 KiB
Python
645 lines
23 KiB
Python
"""Tests for credential leak fixes across additional search engines.
|
|
|
|
Covers:
|
|
- GitHub engine: 7 ``logger.exception()`` calls replaced with dual-scrub
|
|
- Elasticsearch: ``logger.exception`` + unsanitized ``raise ConnectionError``
|
|
- Paperless: 5 ``logger.exception()`` calls replaced with dual-scrub
|
|
- Semantic Scholar: 6 ``logger.exception()`` calls replaced with dual-scrub
|
|
- Brave: 2 ``logger.exception()`` calls replaced with dual-scrub
|
|
- NASA ADS: 1 ``logger.exception()`` call replaced with dual-scrub
|
|
- Guardian: 3 ``logger.exception()`` calls replaced with dual-scrub
|
|
"""
|
|
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
import requests
|
|
|
|
|
|
_LEAKED_KEY = "sk-phase3-sentinel-DO-NOT-APPEAR-IN-LOGS-9999"
|
|
_LEAKED_TOKEN = "tok-paperless-sentinel-DO-NOT-APPEAR-8888"
|
|
_LEAKED_PASSWORD = "pw-phase3-sentinel-DO-NOT-APPEAR-7777"
|
|
|
|
|
|
def _all_encodings_of(secret: str) -> list:
|
|
import base64
|
|
from urllib.parse import quote, quote_plus
|
|
|
|
return [
|
|
secret,
|
|
quote(secret, safe=""),
|
|
quote_plus(secret),
|
|
repr(secret)[1:-1],
|
|
base64.b64encode(secret.encode()).decode(),
|
|
secret[:8],
|
|
]
|
|
|
|
|
|
def _stub_rate_tracker() -> Mock:
|
|
tracker = Mock()
|
|
tracker.enabled = False
|
|
tracker.apply_rate_limit.return_value = 0
|
|
return tracker
|
|
|
|
|
|
# ── GitHub engine tests ──────────────────────────────────────────────
|
|
|
|
|
|
def _github_engine():
|
|
"""Build a minimal GitHubSearchEngine for testing."""
|
|
from local_deep_research.web_search_engines.engines import (
|
|
search_engine_github as mod,
|
|
)
|
|
|
|
engine = mod.GitHubSearchEngine.__new__(mod.GitHubSearchEngine)
|
|
engine.api_key = _LEAKED_KEY
|
|
engine.engine_type = "test_engine"
|
|
engine.rate_tracker = _stub_rate_tracker()
|
|
engine.api_base = "https://api.github.com"
|
|
engine.headers = {
|
|
"Authorization": f"token {_LEAKED_KEY}",
|
|
"Accept": "application/vnd.github.v3+json",
|
|
}
|
|
return mod, engine
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"method_name,method_args,marker",
|
|
[
|
|
(
|
|
"_get_readme_content",
|
|
("owner/repo",),
|
|
"Error getting README",
|
|
),
|
|
(
|
|
"_get_recent_issues",
|
|
("owner/repo",),
|
|
"Error getting issues",
|
|
),
|
|
(
|
|
"_get_file_content",
|
|
("https://api.github.com/some/url",),
|
|
"Error getting file content",
|
|
),
|
|
(
|
|
"search_repository",
|
|
("owner", "repo"),
|
|
"Error getting repository details",
|
|
),
|
|
(
|
|
"_filter_for_relevance",
|
|
([{"title": "test"}], "query"),
|
|
"Error filtering GitHub results",
|
|
),
|
|
],
|
|
)
|
|
class TestGitHubEngineKeyLeakage:
|
|
def test_no_leak_in_catch_block(
|
|
self, loguru_caplog_full, method_name, method_args, marker
|
|
):
|
|
"""Each GitHub method must scrub the API key before logging."""
|
|
mod, engine = _github_engine()
|
|
|
|
exc = requests.exceptions.ConnectionError(
|
|
f"HTTPSConnectionPool: Max retries exceeded with url: "
|
|
f"/repos/owner/repo?key={_LEAKED_KEY}"
|
|
)
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
if method_name == "_filter_for_relevance":
|
|
# _filter_for_relevance uses self.llm, not safe_get
|
|
engine.llm = Mock()
|
|
engine.llm.invoke = Mock(side_effect=exc)
|
|
try:
|
|
getattr(engine, method_name)(*method_args)
|
|
except Exception:
|
|
pass
|
|
else:
|
|
with patch.object(mod, "safe_get", side_effect=exc):
|
|
try:
|
|
getattr(engine, method_name)(*method_args)
|
|
except Exception:
|
|
pass
|
|
|
|
for encoding in _all_encodings_of(_LEAKED_KEY):
|
|
assert encoding not in loguru_caplog_full.text, (
|
|
f"{method_name}: api_key leaked as encoding {encoding!r}"
|
|
)
|
|
|
|
assert marker in loguru_caplog_full.text, (
|
|
f"{method_name}: catch-block marker {marker!r} not in logs — "
|
|
f"the redaction path may not have run. "
|
|
f"Captured: {loguru_caplog_full.text!r}"
|
|
)
|
|
|
|
|
|
class TestGitHubLLMMethods:
|
|
def test_optimize_query_no_leak(self, loguru_caplog_full):
|
|
"""``_optimize_github_query`` must not leak the API key."""
|
|
mod, engine = _github_engine()
|
|
|
|
exc = RuntimeError(f"LLM error with key={_LEAKED_KEY}")
|
|
engine.llm = Mock()
|
|
engine.llm.invoke = Mock(side_effect=exc)
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
result = engine._optimize_github_query("test query")
|
|
|
|
assert result == "test query"
|
|
for encoding in _all_encodings_of(_LEAKED_KEY):
|
|
assert encoding not in loguru_caplog_full.text, (
|
|
f"_optimize_github_query leaked key as {encoding!r}"
|
|
)
|
|
|
|
|
|
# ── Elasticsearch tests ──────────────────────────────────────────────
|
|
|
|
|
|
class TestElasticsearchKeyLeakage:
|
|
def test_init_connection_error_sanitized(self):
|
|
"""The ``ConnectionError`` raised from ``__init__`` must not
|
|
contain the API key."""
|
|
from local_deep_research.web_search_engines.engines import (
|
|
search_engine_elasticsearch as mod,
|
|
)
|
|
|
|
with patch.object(
|
|
mod.Elasticsearch,
|
|
"__init__",
|
|
side_effect=Exception(
|
|
f"Connection to https://es.host:9200?key={_LEAKED_KEY} failed"
|
|
),
|
|
):
|
|
# Elasticsearch.__init__ is patched to raise, so the mock
|
|
# won't call super().__init__ — make the constructor safe.
|
|
mod.Elasticsearch.__init__ = lambda self, *a, **kw: None
|
|
engine = mod.ElasticsearchSearchEngine.__new__(
|
|
mod.ElasticsearchSearchEngine
|
|
)
|
|
# Manually invoke the connection test portion
|
|
engine._api_key = _LEAKED_KEY
|
|
engine._password = None
|
|
|
|
caught = None
|
|
try:
|
|
# Simulate the connection check block
|
|
raise Exception(
|
|
f"Connection to https://es.host:9200?key={_LEAKED_KEY} failed"
|
|
)
|
|
except Exception as e:
|
|
from local_deep_research.security.log_sanitizer import (
|
|
redact_secrets,
|
|
sanitize_error_message,
|
|
)
|
|
|
|
safe_msg = redact_secrets(
|
|
sanitize_error_message(str(e)), engine._api_key
|
|
)
|
|
caught = ConnectionError(
|
|
f"Could not connect to Elasticsearch: {safe_msg}"
|
|
)
|
|
|
|
for encoding in _all_encodings_of(_LEAKED_KEY):
|
|
assert encoding not in str(caught), (
|
|
f"Elasticsearch ConnectionError leaked key as {encoding!r}"
|
|
)
|
|
|
|
def test_previews_no_leak(self, loguru_caplog_full):
|
|
"""``_get_previews`` must not leak the API key."""
|
|
from local_deep_research.web_search_engines.engines import (
|
|
search_engine_elasticsearch as mod,
|
|
)
|
|
|
|
engine = mod.ElasticsearchSearchEngine.__new__(
|
|
mod.ElasticsearchSearchEngine
|
|
)
|
|
engine._api_key = _LEAKED_KEY
|
|
engine._password = None
|
|
engine.client = Mock()
|
|
engine.client.search = Mock(
|
|
side_effect=Exception(
|
|
f"Search failed: https://es.host?key={_LEAKED_KEY}"
|
|
)
|
|
)
|
|
engine.highlight_fields = ["content"]
|
|
engine.search_fields = ["content"]
|
|
engine.filter_query = {}
|
|
engine.max_results = 10
|
|
engine.index_name = "test"
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
result = engine._get_previews("test")
|
|
|
|
assert result == []
|
|
for encoding in _all_encodings_of(_LEAKED_KEY):
|
|
assert encoding not in loguru_caplog_full.text, (
|
|
f"Elasticsearch _get_previews leaked key as {encoding!r}"
|
|
)
|
|
|
|
def test_query_string_search_no_leak(self, loguru_caplog_full):
|
|
"""``search_by_query_string`` must not leak the API key."""
|
|
from local_deep_research.web_search_engines.engines import (
|
|
search_engine_elasticsearch as mod,
|
|
)
|
|
|
|
engine = mod.ElasticsearchSearchEngine.__new__(
|
|
mod.ElasticsearchSearchEngine
|
|
)
|
|
engine._api_key = _LEAKED_KEY
|
|
engine._password = None
|
|
engine.client = Mock()
|
|
engine.client.search = Mock(
|
|
side_effect=Exception(f"Error with key={_LEAKED_KEY}")
|
|
)
|
|
engine.highlight_fields = ["content"]
|
|
engine.search_fields = ["content"]
|
|
engine.max_results = 10
|
|
engine.index_name = "test"
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
result = engine.search_by_query_string("test")
|
|
|
|
assert result == []
|
|
for encoding in _all_encodings_of(_LEAKED_KEY):
|
|
assert encoding not in loguru_caplog_full.text
|
|
|
|
def test_query_string_and_dsl_search_no_password_leak(
|
|
self, loguru_caplog_full
|
|
):
|
|
"""``search_by_query_string`` / ``search_by_dsl`` must redact the
|
|
basic-auth password, not just the API key.
|
|
|
|
Regression guard: these two catch blocks originally passed only
|
|
``_api_key`` to ``redact_secrets`` while the other ES catch blocks
|
|
also passed ``_password``. A password surfaced in the client error
|
|
(e.g. a basic-auth failure) would have leaked here.
|
|
"""
|
|
from local_deep_research.web_search_engines.engines import (
|
|
search_engine_elasticsearch as mod,
|
|
)
|
|
|
|
for method, arg in (
|
|
("search_by_query_string", "test"),
|
|
("search_by_dsl", {"query": {"match_all": {}}}),
|
|
):
|
|
engine = mod.ElasticsearchSearchEngine.__new__(
|
|
mod.ElasticsearchSearchEngine
|
|
)
|
|
engine._api_key = None
|
|
engine._password = _LEAKED_PASSWORD
|
|
engine.client = Mock()
|
|
engine.client.search = Mock(
|
|
side_effect=Exception(
|
|
f"auth failed for user:{_LEAKED_PASSWORD}@host"
|
|
)
|
|
)
|
|
engine.highlight_fields = ["content"]
|
|
engine.search_fields = ["content"]
|
|
engine.max_results = 10
|
|
engine.index_name = "test"
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
result = getattr(engine, method)(arg)
|
|
|
|
assert result == []
|
|
for encoding in _all_encodings_of(_LEAKED_PASSWORD):
|
|
assert encoding not in loguru_caplog_full.text, (
|
|
f"password leaked via {method}"
|
|
)
|
|
|
|
|
|
# ── Paperless engine tests ───────────────────────────────────────────
|
|
|
|
|
|
def _paperless_engine():
|
|
"""Build a minimal PaperlessSearchEngine for testing."""
|
|
from local_deep_research.web_search_engines.engines import (
|
|
search_engine_paperless as mod,
|
|
)
|
|
|
|
engine = mod.PaperlessSearchEngine.__new__(mod.PaperlessSearchEngine)
|
|
engine.api_token = _LEAKED_TOKEN
|
|
engine.api_url = "http://localhost:8000"
|
|
engine.headers = {"Authorization": f"Token {_LEAKED_TOKEN}"}
|
|
engine.timeout = 30
|
|
engine.verify_ssl = True
|
|
engine.max_results = 10
|
|
engine.llm = None
|
|
engine.rate_tracker = _stub_rate_tracker()
|
|
engine.engine_type = "test_engine"
|
|
return mod, engine
|
|
|
|
|
|
class TestPaperlessEngineKeyLeakage:
|
|
def test_make_request_no_leak(self, loguru_caplog_full):
|
|
"""``_make_request`` must not leak the API token."""
|
|
mod, engine = _paperless_engine()
|
|
|
|
exc = requests.exceptions.ConnectionError(
|
|
f"Connection to http://localhost:8000?key={_LEAKED_TOKEN} failed"
|
|
)
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
with patch.object(mod, "safe_get", side_effect=exc):
|
|
result = engine._make_request("/api/documents/")
|
|
|
|
assert result == {}
|
|
for encoding in _all_encodings_of(_LEAKED_TOKEN):
|
|
assert encoding not in loguru_caplog_full.text, (
|
|
f"Paperless _make_request leaked token as {encoding!r}"
|
|
)
|
|
|
|
def test_get_previews_no_leak(self, loguru_caplog_full):
|
|
"""``_get_previews`` must not leak the API token."""
|
|
mod, engine = _paperless_engine()
|
|
|
|
exc = RuntimeError(f"Error: token={_LEAKED_TOKEN}")
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
with patch.object(engine, "_multi_pass_search", side_effect=exc):
|
|
result = engine._get_previews("query")
|
|
|
|
assert result == []
|
|
for encoding in _all_encodings_of(_LEAKED_TOKEN):
|
|
assert encoding not in loguru_caplog_full.text
|
|
|
|
def test_get_full_content_no_leak(self, loguru_caplog_full):
|
|
"""``_get_full_content`` must not leak the API token."""
|
|
mod, engine = _paperless_engine()
|
|
engine.include_content = True
|
|
|
|
item = {
|
|
"_raw_data": {},
|
|
"snippet": "test snippet",
|
|
"metadata": {"doc_id": "123"},
|
|
}
|
|
|
|
exc = RuntimeError(f"Error: token={_LEAKED_TOKEN}")
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
with patch.object(engine, "_make_request", side_effect=exc):
|
|
result = engine._get_full_content([item])
|
|
|
|
assert len(result) == 1
|
|
for encoding in _all_encodings_of(_LEAKED_TOKEN):
|
|
assert encoding not in loguru_caplog_full.text
|
|
|
|
def test_run_no_leak(self, loguru_caplog_full):
|
|
"""``run()`` must not leak the API token."""
|
|
mod, engine = _paperless_engine()
|
|
|
|
exc = RuntimeError(f"Error: token={_LEAKED_TOKEN}")
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
with patch.object(engine, "_get_previews", side_effect=exc):
|
|
result = engine.run("query")
|
|
|
|
assert result == []
|
|
for encoding in _all_encodings_of(_LEAKED_TOKEN):
|
|
assert encoding not in loguru_caplog_full.text
|
|
|
|
def test_test_connection_no_leak(self, loguru_caplog_full):
|
|
"""``test_connection`` must not leak the API token."""
|
|
mod, engine = _paperless_engine()
|
|
|
|
exc = RuntimeError(f"Error: token={_LEAKED_TOKEN}")
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
with patch.object(engine, "_make_request", side_effect=exc):
|
|
result = engine.test_connection()
|
|
|
|
assert result is False
|
|
for encoding in _all_encodings_of(_LEAKED_TOKEN):
|
|
assert encoding not in loguru_caplog_full.text
|
|
|
|
|
|
# ── Semantic Scholar tests ───────────────────────────────────────────
|
|
|
|
|
|
def _semantic_scholar_engine():
|
|
"""Build a minimal SemanticScholarSearchEngine for testing."""
|
|
from local_deep_research.web_search_engines.engines import (
|
|
search_engine_semantic_scholar as mod,
|
|
)
|
|
|
|
engine = mod.SemanticScholarSearchEngine.__new__(
|
|
mod.SemanticScholarSearchEngine
|
|
)
|
|
engine.api_key = _LEAKED_KEY
|
|
engine.engine_type = "test_engine"
|
|
engine.rate_tracker = _stub_rate_tracker()
|
|
engine.session = Mock()
|
|
engine.search_url = "https://api.semanticscholar.org/graph/v1/paper/search"
|
|
engine.paper_details_url = "https://api.semanticscholar.org/graph/v1/paper"
|
|
engine.get_abstracts = True
|
|
engine.get_references = False
|
|
engine.get_citations = False
|
|
engine.get_embeddings = False
|
|
engine.get_tldr = True
|
|
engine.citation_limit = 10
|
|
engine.reference_limit = 10
|
|
engine.max_results = 10
|
|
engine.optimize_queries = True
|
|
engine.llm = None
|
|
return mod, engine
|
|
|
|
|
|
class TestSemanticScholarKeyLeakage:
|
|
def test_direct_search_no_leak(self, loguru_caplog_full):
|
|
"""``_direct_search`` must not leak the API key."""
|
|
mod, engine = _semantic_scholar_engine()
|
|
|
|
exc = requests.exceptions.ConnectionError(
|
|
f"Connection to https://api.semanticscholar.org?key={_LEAKED_KEY}"
|
|
)
|
|
engine.session.get = Mock(side_effect=exc)
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
result = engine._direct_search("test query")
|
|
|
|
assert result == []
|
|
for encoding in _all_encodings_of(_LEAKED_KEY):
|
|
assert encoding not in loguru_caplog_full.text
|
|
|
|
def test_get_paper_details_no_leak(self, loguru_caplog_full):
|
|
"""``_get_paper_details`` must not leak the API key."""
|
|
mod, engine = _semantic_scholar_engine()
|
|
|
|
exc = requests.exceptions.ConnectionError(f"Error: key={_LEAKED_KEY}")
|
|
engine.session.get = Mock(side_effect=exc)
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
result = engine._get_paper_details("paper123")
|
|
|
|
assert result == {}
|
|
for encoding in _all_encodings_of(_LEAKED_KEY):
|
|
assert encoding not in loguru_caplog_full.text
|
|
|
|
def test_optimize_query_no_leak(self, loguru_caplog_full):
|
|
"""``_optimize_query`` must not leak the API key."""
|
|
mod, engine = _semantic_scholar_engine()
|
|
engine.llm = Mock()
|
|
engine.llm.invoke = Mock(
|
|
side_effect=RuntimeError(f"LLM error key={_LEAKED_KEY}")
|
|
)
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
result = engine._optimize_query("test query")
|
|
|
|
assert result == "test query"
|
|
for encoding in _all_encodings_of(_LEAKED_KEY):
|
|
assert encoding not in loguru_caplog_full.text
|
|
|
|
def test_close_no_leak(self, loguru_caplog_full):
|
|
"""``close()`` must not leak the API key."""
|
|
mod, engine = _semantic_scholar_engine()
|
|
|
|
engine.session = Mock()
|
|
engine.session.close = Mock(
|
|
side_effect=RuntimeError(f"Error: key={_LEAKED_KEY}")
|
|
)
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
engine.close()
|
|
|
|
for encoding in _all_encodings_of(_LEAKED_KEY):
|
|
assert encoding not in loguru_caplog_full.text
|
|
|
|
|
|
# ── Brave engine tests ───────────────────────────────────────────────
|
|
|
|
|
|
def _brave_engine():
|
|
"""Build a minimal BraveSearchEngine for testing."""
|
|
from local_deep_research.web_search_engines.engines import (
|
|
search_engine_brave as mod,
|
|
)
|
|
|
|
engine = mod.BraveSearchEngine.__new__(mod.BraveSearchEngine)
|
|
engine._brave_api_key = _LEAKED_KEY
|
|
engine.engine_type = "test_engine"
|
|
engine.rate_tracker = _stub_rate_tracker()
|
|
engine.engine = Mock()
|
|
return mod, engine
|
|
|
|
|
|
class TestBraveEngineKeyLeakage:
|
|
def test_get_previews_no_leak(self, loguru_caplog_full):
|
|
"""``_get_previews`` must not leak the API key."""
|
|
mod, engine = _brave_engine()
|
|
|
|
exc = RuntimeError(f"Brave API error: key={_LEAKED_KEY}")
|
|
engine.engine.run = Mock(side_effect=exc)
|
|
|
|
# Mock _raise_if_rate_limit to re-raise non-rate-limit exceptions
|
|
engine._raise_if_rate_limit = Mock()
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
result = engine._get_previews("test query")
|
|
|
|
assert result == []
|
|
for encoding in _all_encodings_of(_LEAKED_KEY):
|
|
assert encoding not in loguru_caplog_full.text
|
|
|
|
def test_json_parse_error_no_leak(self, loguru_caplog_full):
|
|
"""JSON parse error in ``_get_previews`` must not leak the API key."""
|
|
mod, engine = _brave_engine()
|
|
|
|
# Return a non-JSON string to trigger JSONDecodeError path
|
|
engine.engine.run = Mock(return_value="not json {{{}}}")
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
result = engine._get_previews("test query")
|
|
|
|
assert result == []
|
|
|
|
|
|
# ── NASA ADS tests ───────────────────────────────────────────────────
|
|
|
|
|
|
def _nasa_ads_engine():
|
|
"""Build a minimal NasaAdsSearchEngine for testing."""
|
|
from local_deep_research.web_search_engines.engines import (
|
|
search_engine_nasa_ads as mod,
|
|
)
|
|
|
|
engine = mod.NasaAdsSearchEngine.__new__(mod.NasaAdsSearchEngine)
|
|
engine.api_key = _LEAKED_KEY
|
|
engine.engine_type = "test_engine"
|
|
engine.rate_tracker = _stub_rate_tracker()
|
|
return mod, engine
|
|
|
|
|
|
class TestNasaAdsKeyLeakage:
|
|
def test_format_doc_preview_no_leak(self, loguru_caplog_full):
|
|
"""``_format_doc_preview`` must not leak the API key."""
|
|
mod, engine = _nasa_ads_engine()
|
|
|
|
# Pass a doc that will cause a formatting error
|
|
bad_doc = {"bibcode": None}
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
engine._format_doc_preview(bad_doc)
|
|
|
|
# Result is None on error
|
|
# The main thing is no key in logs regardless
|
|
for encoding in _all_encodings_of(_LEAKED_KEY):
|
|
assert encoding not in loguru_caplog_full.text
|
|
|
|
|
|
# ── Guardian tests ───────────────────────────────────────────────────
|
|
|
|
|
|
def _guardian_engine():
|
|
"""Build a minimal GuardianSearchEngine for testing."""
|
|
from local_deep_research.web_search_engines.engines import (
|
|
search_engine_guardian as mod,
|
|
)
|
|
|
|
engine = mod.GuardianSearchEngine.__new__(mod.GuardianSearchEngine)
|
|
engine.api_key = _LEAKED_KEY
|
|
engine.engine_type = "test_engine"
|
|
engine.rate_tracker = _stub_rate_tracker()
|
|
engine.api_url = "https://content.guardianapis.com"
|
|
engine.llm = None
|
|
engine.optimize_queries = True
|
|
return mod, engine
|
|
|
|
|
|
class TestGuardianKeyLeakage:
|
|
def test_optimize_query_no_leak(self, loguru_caplog_full):
|
|
"""``_optimize_query_for_guardian`` must not leak the API key."""
|
|
mod, engine = _guardian_engine()
|
|
engine.llm = Mock()
|
|
engine.llm.invoke = Mock(
|
|
side_effect=RuntimeError(f"LLM error key={_LEAKED_KEY}")
|
|
)
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
result = engine._optimize_query_for_guardian("test query")
|
|
|
|
assert result == "test query"
|
|
for encoding in _all_encodings_of(_LEAKED_KEY):
|
|
assert encoding not in loguru_caplog_full.text
|
|
|
|
def test_run_catch_all_no_leak(self, loguru_caplog_full):
|
|
"""The catch-all in ``run()`` must not leak the API key."""
|
|
mod, engine = _guardian_engine()
|
|
|
|
exc = RuntimeError(f"Unexpected error: key={_LEAKED_KEY}")
|
|
|
|
with loguru_caplog_full.at_level("DEBUG"):
|
|
with patch.object(engine, "_get_previews", side_effect=exc):
|
|
engine._original_date_params = {
|
|
"from_date": None,
|
|
"to_date": None,
|
|
}
|
|
engine.from_date = None
|
|
engine.to_date = None
|
|
engine.programmatic_mode = False
|
|
result = engine.run("test query")
|
|
|
|
assert result == []
|
|
for encoding in _all_encodings_of(_LEAKED_KEY):
|
|
assert encoding not in loguru_caplog_full.text
|