121 lines
5.0 KiB
Python
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()
|