Files
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

285 lines
10 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
from semantic_kernel.connectors.brave import BraveSearch, BraveSearchResponse, BraveWebPage, BraveWebPages
from semantic_kernel.data.text_search import KernelSearchResults, SearchOptions, TextSearchResult
from semantic_kernel.exceptions import ServiceInitializationError, ServiceInvalidRequestError
@pytest.fixture
def brave_search(brave_unit_test_env):
"""Set up the fixture to configure the brave Search for these tests."""
return BraveSearch()
@pytest.fixture
def async_client_mock():
"""Set up the fixture to mock AsyncClient."""
async_client_mock = AsyncMock()
with patch("semantic_kernel.connectors.brave.AsyncClient.__aenter__", return_value=async_client_mock):
yield async_client_mock
@pytest.fixture
def mock_brave_search_response():
"""Set up the fixture to mock braveSearchResponse."""
mock_web_page = BraveWebPage(name="Page Name", snippet="Page Snippet", url="test")
mock_response = BraveSearchResponse(
query_context={},
webPages=MagicMock(spec=BraveWebPages, value=[mock_web_page], total_estimated_matches=3),
)
with (
patch.object(BraveSearchResponse, "model_validate_json", return_value=mock_response),
):
yield mock_response
async def test_brave_search_init_success(brave_search):
"""Test that braveSearch initializes successfully with valid env."""
# Should not raise any exception
assert brave_search.settings.api_key.get_secret_value() == "test_api_key"
@pytest.mark.parametrize("exclude_list", [["BRAVE_API_KEY"]], indirect=True)
async def test_brave_search_init_validation_error(brave_unit_test_env):
"""Test that braveSearch raises ServiceInitializationError if BraveSettings creation fails."""
with pytest.raises(ServiceInitializationError):
BraveSearch(env_file_path="invalid.env")
async def test_search_success(brave_unit_test_env, async_client_mock):
"""Test that search method returns KernelSearchResults successfully on valid response."""
# Arrange
mock_web_pages = BraveWebPage(description="Test snippet")
mock_response = BraveSearchResponse(
web_pages=MagicMock(spec=BraveWebPages, results=[mock_web_pages]),
query_context={
"original": "original",
"altered": "altered something",
"show_strict_warning": False,
"spellcheck_off": False,
"country": "us",
},
)
mock_result = MagicMock()
mock_result.text = """
{"query": {'original':'original',"altered":
"altered something","show_strict_warning":False,"spellcheck_off":False,'country':"us"},
"results": [{"description": "Test snippet"}]}
}"""
async_client_mock.get.return_value = mock_result
# Act
with (
patch.object(BraveSearchResponse, "model_validate_json", return_value=mock_response),
):
search_instance = BraveSearch()
kernel_results: KernelSearchResults[str] = await search_instance.search("Test query", include_total_count=True)
# Assert
results_list = []
async for res in kernel_results.results:
results_list.append(res)
assert len(results_list) == 1
assert results_list[0] == "Test snippet"
assert kernel_results.total_count == 1
assert kernel_results.metadata == {
"original": "original",
"altered": "altered something",
"show_strict_warning": False,
"spellcheck_off": False,
"country": "us",
}
async def test_search_http_status_error(brave_unit_test_env, async_client_mock):
"""Test that search method raises ServiceInvalidRequestError on HTTPStatusError."""
# Arrange
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
"Error", request=MagicMock(), response=MagicMock()
)
async_client_mock.get.return_value = mock_response
# Act
search_instance = BraveSearch()
# Assert
with pytest.raises(ServiceInvalidRequestError) as exc_info:
await search_instance.search("Test query")
assert "Failed to get search results." in str(exc_info.value)
async def test_search_request_error(brave_unit_test_env, async_client_mock):
"""Test that search method raises ServiceInvalidRequestError on RequestError."""
# Arrange
async_client_mock.get.side_effect = httpx.RequestError("Client error")
# Act
search_instance = BraveSearch()
# Assert
with pytest.raises(ServiceInvalidRequestError) as exc_info:
await search_instance.search("Test query")
assert "A client error occurred while getting search results." in str(exc_info.value)
async def test_search_generic_exception(brave_unit_test_env, async_client_mock):
"""Test that search method raises ServiceInvalidRequestError on unexpected exception."""
# Arrange
async_client_mock.get.side_effect = Exception("Something unexpected")
search_instance = BraveSearch()
# Assert
with pytest.raises(ServiceInvalidRequestError) as exc_info:
await search_instance.search("Test query")
assert "An unexpected error occurred while getting search results." in str(exc_info.value)
async def test_validate_options_raises_error_for_large_top(brave_search):
"""Test that _validate_options raises ServiceInvalidRequestError when top >= 21."""
# Act / Assert
with pytest.raises(ServiceInvalidRequestError) as exc_info:
await brave_search.search("test", top=21)
assert "count value must be less than 21." in str(exc_info.value)
async def test_get_text_search_results_success(brave_unit_test_env, async_client_mock):
"""Test that get_text_search_results returns KernelSearchResults[TextSearchResult]."""
# Arrange
mock_web_pages = BraveWebPage(title="Result Name", description="Test snippet", url="test")
mock_response = BraveSearchResponse(
web_pages=MagicMock(spec=BraveWebPages, results=[mock_web_pages]),
query_context={},
)
mock_result = MagicMock()
mock_result.text = """
{ "results": [{"description": "Test snippet","title":"Result Name","url":"test"}] ,
"query": {}
}
"""
async_client_mock.get.return_value = mock_result
# Act
with (
patch.object(BraveSearchResponse, "model_validate_json", return_value=mock_response),
):
search_instance = BraveSearch()
kernel_results: KernelSearchResults[TextSearchResult] = await search_instance.search(
"Test query", include_total_count=True, output_type=TextSearchResult
)
# Assert
results_list = []
async for res in kernel_results.results:
results_list.append(res)
assert len(results_list) == 1
assert isinstance(results_list[0], TextSearchResult)
assert results_list[0].name == "Result Name"
assert results_list[0].value == "Test snippet"
assert results_list[0].link == "test"
assert kernel_results.total_count == 1
async def test_get_search_results_success(brave_unit_test_env, async_client_mock, mock_brave_search_response):
"""Test that get_search_results returns KernelSearchResults[braveWebPage]."""
# Arrange
mock_web_pages = BraveWebPage(title="Result Name", description="Page snippet", url="test")
mock_response = BraveSearchResponse(
web_pages=MagicMock(spec=BraveWebPages, results=[mock_web_pages]),
query_context={},
)
mock_result = MagicMock()
mock_result.text = """
{ "results": [{"description": "Page snippet","title":"Result Name","url":"test"}] ,
}"""
async_client_mock.get.return_value = mock_result
# Act
with (
patch.object(BraveSearchResponse, "model_validate_json", return_value=mock_response),
):
# Act
search_instance = BraveSearch()
kernel_results = await search_instance.search("Another query", include_total_count=True, output_type="Any")
# Assert
results_list = []
async for res in kernel_results.results:
results_list.append(res)
assert len(results_list) == 1
assert isinstance(results_list[0], BraveWebPage)
assert results_list[0].title == "Result Name"
assert results_list[0].description == "Page snippet"
assert results_list[0].url == "test"
assert kernel_results.total_count == 1
async def test_search_no_filter(brave_search, async_client_mock, mock_brave_search_response):
"""Test that search properly sets params when no filter is provided."""
# Arrange
options = SearchOptions()
# Act
await brave_search.search("test query")
# Assert
params = async_client_mock.get.call_args.kwargs["params"]
assert params["count"] == options.top
assert params["offset"] == options.skip
# TODO check: shouldn't this output be "test query" instead of "test query+"?
assert params["q"] == "test query"
@pytest.mark.parametrize(
"filter_lambda,expected",
[
("lambda x: x.country == 'US'", [{"country": "US"}]),
("lambda x: x.search_lang == 'en'", [{"search_lang": "en"}]),
("lambda x: x.ui_lang == 'fr'", [{"ui_lang": "fr"}]),
("lambda x: x.safesearch == 'strict'", [{"safesearch": "strict"}]),
("lambda x: x.text_decorations == '1'", [{"text_decorations": "1"}]),
("lambda x: x.spellcheck == '0'", [{"spellcheck": "0"}]),
("lambda x: x.result_filter == 'web'", [{"result_filter": "web"}]),
("lambda x: x.units == 'metric'", [{"units": "metric"}]),
("lambda x: x.country == 'US' and x.ui_lang == 'fr'", [{"country": "US"}, {"ui_lang": "fr"}]),
(lambda x: x.country == "US" and x.ui_lang == "fr", [{"country": "US"}, {"ui_lang": "fr"}]),
],
)
def test_parse_filter_lambda_valid(brave_search, filter_lambda, expected):
assert brave_search._parse_filter_lambda(filter_lambda) == expected
@pytest.mark.parametrize(
"filter_lambda,exception_type",
[
("lambda x: x.country != 'US'", NotImplementedError),
("lambda x: x.country == y", NotImplementedError),
("lambda x: x.country == None", NotImplementedError),
("lambda x: x.country > 'US'", NotImplementedError),
("lambda x: x.unknown == 'foo'", ValueError),
("lambda x: x.country == 'US' or x.ui_lang == 'fr'", NotImplementedError),
("lambda x: x.cr == 'US'", ValueError), # not in Brave QUERY_PARAMETERS
("lambda x: x.lr == 'lang_en'", ValueError), # not in Brave QUERY_PARAMETERS
],
)
def test_parse_filter_lambda_invalid(brave_search, filter_lambda, exception_type):
with pytest.raises(exception_type):
brave_search._parse_filter_lambda(filter_lambda)