chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,186 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from httpx import HTTPStatusError, RequestError, Response
|
||||
|
||||
from semantic_kernel.connectors.google_search import (
|
||||
GoogleSearch,
|
||||
GoogleSearchInformation,
|
||||
GoogleSearchResponse,
|
||||
GoogleSearchResult,
|
||||
)
|
||||
from semantic_kernel.data.text_search import TextSearchResult
|
||||
from semantic_kernel.exceptions import ServiceInitializationError, ServiceInvalidRequestError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def google_search(google_search_unit_test_env):
|
||||
"""Fixture to return a GoogleSearch instance with valid settings."""
|
||||
return GoogleSearch()
|
||||
|
||||
|
||||
async def test_google_search_init_success(google_search) -> None:
|
||||
"""Test that GoogleSearch successfully initializes with valid parameters."""
|
||||
# Should not raise any exception
|
||||
assert google_search.settings.api_key.get_secret_value() == "test_api_key"
|
||||
assert google_search.settings.engine_id == "test_id"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exclude_list", [["GOOGLE_SEARCH_API_KEY"]], indirect=True)
|
||||
async def test_google_search_init_validation_error(google_search_unit_test_env) -> None:
|
||||
"""Test that GoogleSearch raises ServiceInitializationError when GoogleSearchSettings creation fails."""
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
GoogleSearch(env_file_path="invalid.env")
|
||||
|
||||
|
||||
async def test_google_search_top_greater_than_10_raises_error(google_search) -> None:
|
||||
"""Test that passing a top value greater than 10 raises ServiceInvalidRequestError."""
|
||||
with pytest.raises(ServiceInvalidRequestError) as exc:
|
||||
await google_search.search(query="test query", top=11)
|
||||
assert "count value must be less than or equal to 10." in str(exc.value)
|
||||
|
||||
|
||||
async def test_google_search_no_items_in_response(google_search) -> None:
|
||||
"""Test that when the response has no items, search results yield nothing."""
|
||||
mock_response = GoogleSearchResponse(items=None)
|
||||
|
||||
# We'll mock _inner_search to return our mock_response
|
||||
with patch.object(google_search, "_inner_search", new=AsyncMock(return_value=mock_response)):
|
||||
result = await google_search.search("test")
|
||||
# Extract all items from the AsyncIterable
|
||||
items = [item async for item in result.results]
|
||||
assert len(items) == 0
|
||||
|
||||
|
||||
async def test_google_search_partial_items_in_response(google_search) -> None:
|
||||
"""Test that snippets are properly returned in search results."""
|
||||
snippet_1 = "Snippet 1"
|
||||
snippet_2 = "Snippet 2"
|
||||
item_1 = GoogleSearchResult(snippet=snippet_1)
|
||||
item_2 = GoogleSearchResult(snippet=snippet_2)
|
||||
mock_response = GoogleSearchResponse(items=[item_1, item_2])
|
||||
|
||||
with patch.object(google_search, "_inner_search", new=AsyncMock(return_value=mock_response)):
|
||||
result = await google_search.search("test")
|
||||
items = [item async for item in result.results]
|
||||
assert len(items) == 2
|
||||
assert items[0] == snippet_1
|
||||
assert items[1] == snippet_2
|
||||
|
||||
|
||||
async def test_google_search_request_http_status_error(google_search) -> None:
|
||||
"""Test that HTTP status errors raise ServiceInvalidRequestError."""
|
||||
# Mock the AsyncClient.get call to raise HTTPStatusError
|
||||
with patch(
|
||||
"httpx.AsyncClient.get",
|
||||
new=AsyncMock(side_effect=HTTPStatusError("Error", request=None, response=Response(status_code=400))),
|
||||
):
|
||||
with pytest.raises(ServiceInvalidRequestError) as exc:
|
||||
await google_search.search("query")
|
||||
assert "Failed to get search results." in str(exc.value)
|
||||
|
||||
|
||||
async def test_google_search_request_error(google_search) -> None:
|
||||
"""Test that request errors raise ServiceInvalidRequestError."""
|
||||
# Mock the AsyncClient.get call to raise RequestError
|
||||
with patch("httpx.AsyncClient.get", new=AsyncMock(side_effect=RequestError("Client error"))):
|
||||
with pytest.raises(ServiceInvalidRequestError) as exc:
|
||||
await google_search.search("query")
|
||||
assert "A client error occurred while getting search results." in str(exc.value)
|
||||
|
||||
|
||||
async def test_google_search_unexpected_error(google_search) -> None:
|
||||
"""Test that unexpected exceptions raise ServiceInvalidRequestError."""
|
||||
# Mock the AsyncClient.get call to raise a random exception
|
||||
with patch("httpx.AsyncClient.get", new=AsyncMock(side_effect=Exception("Random error"))):
|
||||
with pytest.raises(ServiceInvalidRequestError) as exc:
|
||||
await google_search.search("query")
|
||||
assert "An unexpected error occurred while getting search results." in str(exc.value)
|
||||
|
||||
|
||||
async def test_get_text_search_results(google_search) -> None:
|
||||
"""Test that get_text_search_results returns TextSearchResults that contain name, value, and link."""
|
||||
item_1 = GoogleSearchResult(title="Title1", snippet="Snippet1", link="Link1")
|
||||
item_2 = GoogleSearchResult(title="Title2", snippet="Snippet2", link="Link2")
|
||||
mock_response = GoogleSearchResponse(items=[item_1, item_2])
|
||||
|
||||
with patch.object(google_search, "_inner_search", new=AsyncMock(return_value=mock_response)):
|
||||
result = await google_search.search("test", output_type=TextSearchResult)
|
||||
items = [item async for item in result.results]
|
||||
assert len(items) == 2
|
||||
assert items[0].name == "Title1"
|
||||
assert items[0].value == "Snippet1"
|
||||
assert items[0].link == "Link1"
|
||||
assert items[1].name == "Title2"
|
||||
assert items[1].value == "Snippet2"
|
||||
assert items[1].link == "Link2"
|
||||
|
||||
|
||||
async def test_get_search_results(google_search) -> None:
|
||||
"""Test that get_search_results returns GoogleSearchResult items directly."""
|
||||
item_1 = GoogleSearchResult(title="Title1", snippet="Snippet1", link="Link1")
|
||||
item_2 = GoogleSearchResult(title="Title2", snippet="Snippet2", link="Link2")
|
||||
mock_response = GoogleSearchResponse(items=[item_1, item_2])
|
||||
|
||||
with patch.object(google_search, "_inner_search", new=AsyncMock(return_value=mock_response)):
|
||||
result = await google_search.search("test", output_type="Any")
|
||||
items = [item async for item in result.results]
|
||||
assert len(items) == 2
|
||||
assert items[0].title == "Title1"
|
||||
assert items[1].link == "Link2"
|
||||
|
||||
|
||||
async def test_google_search_includes_total_count(google_search) -> None:
|
||||
"""Test that total_count is included if include_total_count is True."""
|
||||
search_info = GoogleSearchInformation(
|
||||
searchTime=0.23, totalResults="42", formattedSearchTime="0.23s", formattedTotalResults="42"
|
||||
)
|
||||
mock_response = GoogleSearchResponse(search_information=search_info, items=None)
|
||||
|
||||
with patch.object(google_search, "_inner_search", new=AsyncMock(return_value=mock_response)):
|
||||
result = await google_search.search(query="test query", include_total_count=True)
|
||||
assert result.total_count == 42
|
||||
# if we set it to false, total_count should be None
|
||||
result_no_count = await google_search.search(query="test query", include_total_count=False)
|
||||
assert result_no_count.total_count is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filter_lambda,expected",
|
||||
[
|
||||
("lambda x: x.cr == 'US'", [{"cr": "US"}]),
|
||||
("lambda x: x.dateRestrict == 'd7'", [{"dateRestrict": "d7"}]),
|
||||
("lambda x: x.fileType == 'pdf'", [{"fileType": "pdf"}]),
|
||||
("lambda x: x.lr == 'lang_en'", [{"lr": "lang_en"}]),
|
||||
("lambda x: x.orTerms == 'foo bar'", [{"orTerms": "foo+bar"}]),
|
||||
("lambda x: x.siteSearch == 'example.com'", [{"siteSearch": "example.com"}]),
|
||||
("lambda x: x.siteSearchFilter == 'e'", [{"siteSearchFilter": "e"}]),
|
||||
("lambda x: x.rights == 'cc_publicdomain'", [{"rights": "cc_publicdomain"}]),
|
||||
("lambda y: y.rights == 'cc_publicdomain'", [{"rights": "cc_publicdomain"}]),
|
||||
("lambda x: x.hl == 'en'", [{"hl": "en"}]),
|
||||
("lambda x: x.filter == '1'", [{"filter": "1"}]),
|
||||
("lambda x: x.cr == 'US' and x.lr == 'lang_en'", [{"cr": "US"}, {"lr": "lang_en"}]),
|
||||
(lambda x: x.cr == "US" and x.lr == "lang_en", [{"cr": "US"}, {"lr": "lang_en"}]),
|
||||
("x.cr == 'US'", [{"cr": "US"}]),
|
||||
],
|
||||
)
|
||||
def test_parse_filter_lambda_valid(google_search, filter_lambda, expected):
|
||||
assert google_search._parse_filter_lambda(filter_lambda) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filter_lambda,exception_type",
|
||||
[
|
||||
("lambda x: x.cr != 'US'", NotImplementedError),
|
||||
("lambda x: x.cr == y", NotImplementedError),
|
||||
("lambda x: x.cr == None", NotImplementedError),
|
||||
("lambda x: x.cr > 'US'", NotImplementedError),
|
||||
("lambda x: x.unknown == 'foo'", ValueError),
|
||||
("lambda x: x.cr == 'US' or x.lr == 'lang_en'", NotImplementedError),
|
||||
],
|
||||
)
|
||||
def test_parse_filter_lambda_invalid(google_search, filter_lambda, exception_type):
|
||||
with pytest.raises(exception_type):
|
||||
google_search._parse_filter_lambda(filter_lambda)
|
||||
Reference in New Issue
Block a user