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