import os
os.environ["LLM_SSL_VERIFY"] = "false"
os.environ["TOOL_SSL_VERIFY"] = "false"
from unittest.mock import Mock, patch, AsyncMock
import pytest
import requests
from pydantic import SecretStr
from openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.serper.api_wrapper import GoogleSearchAPIWrapper
class TestGoogleSearchAPIWrapper:
def setup_method(self):
"""在每个测试方法前执行"""
self.mock_api_key = bytearray(b"test-api-key-123")
self.mock_search_url = SecretStr("https://api.serper.dev")
self.wrapper = GoogleSearchAPIWrapper(
search_api_key=self.mock_api_key,
search_url=self.mock_search_url,
max_web_search_results=5
)
def test_init(self):
"""测试初始化"""
assert self.wrapper.search_api_key == bytearray(b"test-api-key-123")
assert self.wrapper.search_url.get_secret_value() == "https://api.serper.dev"
assert self.wrapper.max_web_search_results == 5
assert self.wrapper.type == "search"
assert self.wrapper.gl == "us"
assert self.wrapper.hl == "en"
@patch('openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.serper.api_wrapper.requests.post')
@patch('openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.serper.api_wrapper.SslUtils.get_ssl_config')
def test_google_search_results_success(self, mock_ssl_config, mock_post):
"""测试同步搜索功能 - 成功情况"""
mock_ssl_config.return_value = (True, "/path/to/cert")
mock_response = Mock()
mock_response.status_code = 200
mock_response.raise_for_status.return_value = None
mock_response.json.return_value = {
"organic": [{"title": "Test Result", "link": "http://example.com"}]
}
mock_post.return_value = mock_response
result = self.wrapper.google_search_results("test query")
mock_post.assert_called_once()
call_args = mock_post.call_args
assert "https://api.serper.dev/search" in str(call_args[0])
headers = call_args[1]['headers']
assert headers['X-API-KEY'] == "test-api-key-123"
assert headers['Content-Type'] == "application/json"
payload = call_args[1]['json']
assert payload['q'] == "test query"
assert payload['gl'] == "us"
assert call_args[1]['verify'] == "/path/to/cert"
assert isinstance(result, list)
assert len(result) == 1
assert result[0]["title"] == "Test Result"
@patch('openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.serper.api_wrapper.requests.post')
@patch('openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.serper.api_wrapper.SslUtils.get_ssl_config')
def test_google_search_results_uses_default_url_when_empty(self, mock_ssl_config, mock_post):
"""Serper should use the public default endpoint when search_url is empty."""
mock_ssl_config.return_value = (False, None)
mock_response = Mock()
mock_response.status_code = 200
mock_response.raise_for_status.return_value = None
mock_response.json.return_value = {"organic": []}
mock_post.return_value = mock_response
wrapper = GoogleSearchAPIWrapper(
search_api_key=bytearray(b"test-api-key-123"),
search_url="",
max_web_search_results=3,
)
wrapper.google_search_results("test query")
call_args = mock_post.call_args
assert call_args[0][0] == "https://google.serper.dev/search"
assert call_args[1]["json"] == {
"q": "test query",
"gl": "us",
"hl": "en",
"num": 3,
}
@patch('openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.serper.api_wrapper.requests.post')
@patch('openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.serper.api_wrapper.SslUtils.get_ssl_config')
def test_google_search_results_with_custom_type(self, mock_ssl_config, mock_post):
"""测试不同类型的搜索"""
mock_ssl_config.return_value = (False, None)
mock_response = Mock()
mock_response.status_code = 200
mock_response.raise_for_status.return_value = None
mock_response.json.return_value = {
"news": [{"title": "News Result"}]
}
mock_post.return_value = mock_response
self.wrapper.type = "news"
result = self.wrapper.google_search_results("test query")
assert isinstance(result, list)
assert result[0]["title"] == "News Result"
call_args = mock_post.call_args
assert call_args[1]['verify'] is False
@patch('openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.serper.api_wrapper.requests.post')
def test_google_search_results_with_exception(self, mock_post):
"""测试搜索时的异常处理"""
mock_response = Mock()
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("API Error")
mock_post.return_value = mock_response
with pytest.raises(requests.exceptions.HTTPError):
self.wrapper.google_search_results("test query")
@patch(
'openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.serper.api_wrapper.'
'GoogleSearchAPIWrapper.google_search_results'
)
def test_results_method(self, mock_search_results):
"""测试results包装方法"""
expected_result = [{"title": "Test"}]
mock_search_results.return_value = expected_result
result = self.wrapper.results("test query")
mock_search_results.assert_called_once_with(search_term="test query")
assert result == expected_result
@pytest.mark.asyncio
@patch('openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.serper.api_wrapper.SslUtils.get_ssl_config')
async def test_async_google_search_results(self, mock_ssl_config):
"""测试异步搜索功能"""
mock_ssl_config.return_value = (True, "/path/to/cert")
mock_response = Mock()
mock_response.json.return_value = {
"organic": [{"title": "Async Result", "link": "http://example.com"}]
}
mock_client = AsyncMock()
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
mock_client.post.return_value = mock_response
self.wrapper.gl = "cn"
self.wrapper.hl = "zh-cn"
with patch('openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.serper.api_wrapper.httpx.AsyncClient',
return_value=mock_client):
result = await self.wrapper.async_google_search_results("async query")
mock_client.post.assert_called_once()
call_args = mock_client.post.call_args
assert "https://api.serper.dev/search" in str(call_args[0])
headers = call_args[1]['headers']
assert headers['X-API-KEY'] == "test-api-key-123"
payload = call_args[1]['json']
assert payload['q'] == "async query"
assert payload['gl'] == "cn"
assert payload['hl'] == "zh-cn"
assert isinstance(result, list)
assert result[0]["title"] == "Async Result"
@pytest.mark.asyncio
async def test_async_google_search_results_with_ssl_false(self):
"""测试SSL验证为False的异步搜索"""
with patch(
'openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.serper.api_wrapper.'
'SslUtils.get_ssl_config'
) as mock_ssl_config:
mock_ssl_config.return_value = (False, None)
mock_response = Mock()
mock_response.json.return_value = {"organic": []}
mock_client = AsyncMock()
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
mock_client.post.return_value = mock_response
with patch(
'openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.serper.api_wrapper.httpx.AsyncClient',
return_value=mock_client):
result = await self.wrapper.async_google_search_results("test query")
mock_client.post.assert_called_once()
call_kwargs = mock_client.post.call_args[1]
pass
@pytest.mark.asyncio
@patch(
'openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.serper.api_wrapper.'
'GoogleSearchAPIWrapper.async_google_search_results'
)
async def test_aresults_method(self, mock_async_search_results):
"""测试异步包装方法"""
expected_result = [{"title": "Async Test"}]
mock_async_search_results.return_value = expected_result
result = await self.wrapper.aresults("test query")
mock_async_search_results.assert_called_once_with(search_term="test query")
assert result == expected_result
def test_result_key_mapping(self):
"""测试不同类型对应的结果键名"""
assert self.wrapper.result_key_for_type["news"] == "news"
assert self.wrapper.result_key_for_type["search"] == "organic"
assert self.wrapper.result_key_for_type["places"] == "places"
assert self.wrapper.result_key_for_type["images"] == "images"
def test_model_config(self):
"""测试模型配置"""
wrapper_with_extra = GoogleSearchAPIWrapper(
search_api_key=bytearray(b"key"),
search_url=SecretStr("url"),
extra_field="allowed"
)
assert hasattr(wrapper_with_extra, 'extra_field')
def test_search_api_key_decoding(self):
"""测试API key解码"""
api_key = bytearray(b"test-key")
wrapper = GoogleSearchAPIWrapper(
search_api_key=api_key,
search_url=SecretStr("url")
)
decoded_key = api_key.decode('utf-8')
assert decoded_key == "test-key"
def test_search_url_secret(self):
"""测试SecretStr处理"""
secret_url = SecretStr("https://secret.api.url")
wrapper = GoogleSearchAPIWrapper(
search_api_key=bytearray(b"key"),
search_url=secret_url
)
assert wrapper.search_url.get_secret_value() == "https://secret.api.url"
def test_extension_search_type(self):
"""测试extension中search_type可配置默认搜索类型"""
wrapper = GoogleSearchAPIWrapper(
search_api_key=bytearray(b"test-key"),
search_url=SecretStr("https://api.serper.dev"),
extension={"type": "news"},
)
assert wrapper.type == "news"
def test_web_search_mapping_uses_builtin_serper_wrapper_for_google_and_serper():
"""Research mapping should route google/serper to the repository Serper wrapper."""
from openjiuwen_deepsearch.framework.openjiuwen.tools.search_api.serper.api_wrapper import (
GoogleSearchAPIWrapper,
)
from openjiuwen_deepsearch.framework.openjiuwen.tools.web_search import search_engine_mapping
assert search_engine_mapping["google"] is GoogleSearchAPIWrapper
assert search_engine_mapping["serper"] is GoogleSearchAPIWrapper