assistant/tests/test_public_research.py

121 lines
5.0 KiB
Python

"""Public research stays separate from logged-in browser evidence."""
from __future__ import annotations
import json
import socket
import httpx
import pytest
from backend import research
@pytest.fixture
def provider(monkeypatch):
original = httpx.AsyncClient
requests: list[httpx.Request] = []
result = {"status": 200, "body": {"results": []}}
def handle(request):
requests.append(request)
return httpx.Response(result["status"], json=result["body"])
monkeypatch.setattr(research, "_api_key", lambda: "TEST-PRIVATE-KEY")
monkeypatch.setattr(research.httpx, "AsyncClient", lambda **kwargs: original(transport=httpx.MockTransport(handle), **kwargs))
return requests, result
async def test_search_calls_fixed_endpoint_and_labels_summary_as_public(provider):
requests, response = provider
response["body"] = {"results": [{"title": "公开资料", "url": "https://example.com/post", "summary": "摘要"}]}
result = await research.search_public("小红书 服务需求", 3)
assert str(requests[0].url) == "https://api.deepseek.com/v1/tools/search"
assert json.loads(requests[0].content) == {"query": "小红书 服务需求", "count": 3, "need_content": False}
assert requests[0].headers["authorization"] == "Bearer TEST-PRIVATE-KEY"
assert result["source_type"] == "public_web_search"
assert result["results"][0]["summary"] == "摘要"
assert "TEST-PRIVATE-KEY" not in json.dumps(result)
@pytest.mark.parametrize("query,count", [("", 3), ("x" * 129, 3), ("词", 0), ("词", 11), ("词", True)])
async def test_invalid_search_does_not_call_provider(query, count, provider):
requests, _response = provider
with pytest.raises(ValueError):
await research.search_public(query, count)
assert requests == []
async def test_missing_results_is_a_failure_not_an_empty_success(provider):
_requests, response = provider
response["body"] = {"message": "not a search response"}
with pytest.raises(research.ResearchError, match="results"):
await research.search_public("关键词")
async def test_search_filters_local_result_links(provider):
_requests, response = provider
response["body"] = {"results": [
{"url": "http://127.0.0.1/admin"},
{"url": "https://example.com/ok"},
]}
result = await research.search_public("关键词")
assert len(result["results"]) == 1
assert result["dropped_results"] == 1
@pytest.mark.parametrize("url", [
"file:///etc/hosts", "http://localhost/", "http://127.0.0.1/", "https://[::1]/",
"http://10.0.0.2/", "http://169.254.169.254/", "https://host.local/",
"http://user:pass@example.com/", "https://example.com:8443/", "http://2130706433/",
])
async def test_fetch_rejects_nonpublic_targets_before_provider(url, provider):
requests, _response = provider
with pytest.raises(ValueError):
await research.fetch_public(url)
assert requests == []
async def test_fetch_rejects_hostname_resolving_to_private_address(monkeypatch, provider):
requests, _response = provider
monkeypatch.setattr(socket, "getaddrinfo", lambda *args, **kwargs: [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.168.1.8", 443))])
with pytest.raises(ValueError, match="解析"):
await research.fetch_public("https://public-looking.example.com/")
assert requests == []
async def test_fetch_caps_content_and_preserves_source(provider):
requests, response = provider
response["body"] = {"title": "长资料", "content": "文" * 24_001}
result = await research.fetch_public("https://93.184.216.34/page#fragment")
assert json.loads(requests[0].content) == {"url": "https://93.184.216.34/page"}
assert result["source_type"] == "public_web_fetch"
assert len(result["content"]) == 24_000
assert result["content_truncated"] is True
async def test_provider_error_does_not_leak_body_or_automatically_retry(provider):
requests, response = provider
response.update(status=401, body={"debug": "TEST-PRIVATE-KEY"})
with pytest.raises(research.ResearchError) as error:
await research.search_public("关键词")
assert error.value.status_code == 401
assert "TEST-PRIVATE-KEY" not in str(error.value)
assert len(requests) == 1
async def test_missing_key_is_actionable_and_does_not_send_request(monkeypatch):
monkeypatch.setattr(research, "get_llm_config", lambda: {"provider": "deepseek", "api_key": ""})
with pytest.raises(research.ResearchError, match="API Key") as error:
await research.search_public("关键词")
assert error.value.status_code == 503
def test_qwen_key_cannot_be_used_as_deepseek_research_key(monkeypatch):
monkeypatch.setattr(research, "get_llm_config", lambda: {"provider": "qwen", "api_key": "QWEN-ONLY"})
monkeypatch.setattr(research.db, "get_setting", lambda *args: "")
monkeypatch.setattr(research.settings, "deepseek_api_key", "")
monkeypatch.setattr(research.llm_keys, "choose", lambda provider, primary: (primary, False))
with pytest.raises(research.ResearchError, match="DeepSeek"):
research._api_key()