blob: 74ed21fea76ad2786251ca0be7dd5a1f973e4fde [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <string>
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/location_bar/location_bar.h"
#include "chrome/browser/ui/omnibox/omnibox_controller.h"
#include "chrome/browser/ui/omnibox/omnibox_edit_model.h"
#include "chrome/browser/ui/omnibox/omnibox_view.h"
#include "chrome/browser/ui/search/omnibox_utils.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/search_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/omnibox/browser/autocomplete_controller.h"
#include "components/omnibox/browser/autocomplete_input.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/autocomplete_result.h"
#include "components/omnibox/browser/enterprise_search_aggregator_suggestions_service.h"
#include "components/omnibox/common/omnibox_feature_configs.h"
#include "components/policy/core/browser/browser_policy_connector.h"
#include "components/policy/core/common/mock_configuration_policy_provider.h"
#include "components/policy/policy_constants.h"
#include "components/search_engines/enterprise/search_aggregator_policy_handler.h"
#include "components/search_engines/search_engine_type.h"
#include "components/search_engines/template_url_service.h"
#include "content/public/test/browser_test.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "third_party/metrics_proto/omnibox_event.pb.h"
using testing::AllOf;
using testing::Field;
namespace {
using ui_test_utils::WaitForAutocompleteDone;
const char kSearchAggregatorPolicyIconUrl[] =
"https://www.aggregator.com/icon.png";
const char16_t kSearchAggregatorPolicyKeyword[] = u"aggregator";
const char16_t kSearchAggregatorPolicyName[] = u"Aggregator";
const char kSearchAggregatorPolicySearchUrl[] =
"https://www.aggregator.com/search?q={searchTerms}";
const char kSearchAggregatorPolicySuggestUrl[] =
"https://www.aggregator.com/suggest";
const char kSearchAggregatorPolicySuggestPath[] = "/suggest";
const std::u16string kSearchInput = u"@aggregator john d";
const std::string kGoodJsonResponse = base::StringPrintf(
R"({
"querySuggestions": [
{
"suggestion": "John's Demise",
"score": 0.1,
"dataStore": []
}
],
"peopleSuggestions": [
{
"suggestion": "john@example.com",
"document": {
"name": "sundar",
"derivedStructData": {
"name": {
"display_name_lower": "john doe",
"familyName": "Doe",
"givenName": "John",
"given_name_lower": "john",
"family_name_lower": "doe",
"displayName": "John Doe",
"userName": "john"
},
"emails": [
{
"type": "primary",
"value": "john@example.com"
}
],
"displayPhoto": {
"url": "https://example.com/image.png"
}
}
},
"destinationUri": "https://www.example.com/people/john",
"score": 0.8,
"dataStore": "project 1"
}
],
"contentSuggestions": [
{
"suggestion": "John's Document",
"contentType": "THIRD_PARTY",
"document": {
"name": "Document 2",
"structData": {
"title": "John's Document",
"uri": "www.example.com"
},
"derivedStructData": {
"source_type": "jira",
"entity_type": "issue",
"title": "John's Document",
"link": "https://www.example.co.uk"
}
},
"destinationUri": "https://www.example.com/",
"score": 0.4,
"dataStore": "project2"
}
]
})");
} // namespace
class OmniboxSearchAggregatorTest : public InProcessBrowserTest {
public:
OmniboxSearchAggregatorTest() = default;
~OmniboxSearchAggregatorTest() override = default;
void SetUp() override {
policy_provider_.SetDefaultReturns(
/*is_initialization_complete_return=*/true,
/*is_first_policy_load_complete_return=*/true);
policy::BrowserPolicyConnector::SetPolicyProviderForTesting(
&policy_provider_);
InProcessBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
InProcessBrowserTest::SetUpOnMainThread();
// The omnibox suggestion results depend on the TemplateURLService being
// loaded. Make sure it is loaded so that the autocomplete results are
// consistent.
search_test_utils::WaitForTemplateURLServiceToLoad(
TemplateURLServiceFactory::GetForProfile(browser()->profile()));
// Prevent the stop timer from killing the hints fetch early, which might
// cause test flakiness due to timeout.
controller()->SetStartStopTimerDurationForTesting(base::Seconds(30));
// Setup an identity profile.
identity_test_env_adaptor_ =
std::make_unique<IdentityTestEnvironmentProfileAdaptor>(
browser()->profile());
identity_test_env()->SetPrimaryAccount("test@mail.com",
signin::ConsentLevel::kSignin);
identity_test_env()->SetRefreshTokenForPrimaryAccount();
identity_test_env()->SetAutomaticIssueOfAccessTokens(true);
// Initial the HTTPS embedded test server.
https_test_server_ = std::make_unique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::TYPE_HTTPS);
https_test_server_->RegisterRequestHandler(base::BindRepeating(
&OmniboxSearchAggregatorTest::HandleRequest, base::Unretained(this)));
}
void SetUpInProcessBrowserTestFixture() override {
create_services_subscription_ =
BrowserContextDependencyManager::GetInstance()
->RegisterCreateServicesCallbackForTesting(
base::BindRepeating(&OmniboxSearchAggregatorTest::
OnWillCreateBrowserContextServices,
base::Unretained(this)));
}
void OnWillCreateBrowserContextServices(content::BrowserContext* context) {
IdentityTestEnvironmentProfileAdaptor::
SetIdentityTestEnvironmentFactoriesOnBrowserContext(context);
}
protected:
std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
const net::test_server::HttpRequest& request) {
// The kSuggestUrl will be hit by GET request from SearchProvider, we could
// ignore it for SearchAggregator tests.
if (request.relative_url == kSearchAggregatorPolicySuggestPath &&
request.method == net::test_server::METHOD_GET) {
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_NOT_FOUND);
return std::move(http_response);
}
return nullptr;
}
base::Value CreateEnterpriseSearchAggregatorPolicyValue(
const std::string& suggest_url = kSearchAggregatorPolicySuggestUrl) {
base::Value::Dict policy_value;
policy_value = base::Value::Dict()
.Set(policy::SearchAggregatorPolicyHandler::kIconUrl,
kSearchAggregatorPolicyIconUrl)
.Set(policy::SearchAggregatorPolicyHandler::kShortcut,
kSearchAggregatorPolicyKeyword)
.Set(policy::SearchAggregatorPolicyHandler::kName,
kSearchAggregatorPolicyName)
.Set(policy::SearchAggregatorPolicyHandler::kSearchUrl,
kSearchAggregatorPolicySearchUrl)
.Set(policy::SearchAggregatorPolicyHandler::kSuggestUrl,
suggest_url);
return base::Value(std::move(policy_value));
}
AutocompleteController* controller() {
return browser()
->window()
->GetLocationBar()
->GetOmniboxView()
->controller()
->autocomplete_controller();
}
// Override embedded_test_server() with a variant that uses HTTPS.
net::EmbeddedTestServer* embedded_test_server() {
return https_test_server_.get();
}
policy::MockConfigurationPolicyProvider* policy_provider() {
return &policy_provider_;
}
signin::IdentityTestEnvironment* identity_test_env() {
return identity_test_env_adaptor_->identity_test_env();
}
omnibox_feature_configs::ScopedConfigForTesting<
omnibox_feature_configs::SearchAggregatorProvider>
scoped_config_;
private:
testing::NiceMock<policy::MockConfigurationPolicyProvider> policy_provider_;
// SearchAggregatorPolicy requires HTTPS scheme.
std::unique_ptr<net::EmbeddedTestServer> https_test_server_;
base::CallbackListSubscription create_services_subscription_;
std::unique_ptr<IdentityTestEnvironmentProfileAdaptor>
identity_test_env_adaptor_;
};
class OmniboxSearchAggregatorSingleRequestTest
: public OmniboxSearchAggregatorTest {
public:
void SetUp() override {
scoped_config_.Get().multiple_requests = false;
OmniboxSearchAggregatorTest::SetUp();
}
};
// TODO(crbug.com/425120649) Flaky.
IN_PROC_BROWSER_TEST_F(OmniboxSearchAggregatorSingleRequestTest,
DISABLED_GoodJsonResponse) {
net::test_server::ControllableHttpResponse search_aggregator_response(
embedded_test_server(), kSearchAggregatorPolicySuggestPath);
ASSERT_TRUE(embedded_test_server()->Start());
base::Value policy_value = CreateEnterpriseSearchAggregatorPolicyValue(
embedded_test_server()
->GetURL(kSearchAggregatorPolicySuggestPath)
.spec());
policy::PolicyMap policies;
policies.Set(policy::key::kEnterpriseSearchAggregatorSettings,
policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_USER,
policy::POLICY_SOURCE_CLOUD, std::move(policy_value), nullptr);
policy_provider()->UpdateChromePolicy(policies);
AutocompleteInput input(
kSearchInput, metrics::OmniboxEventProto::NTP,
ChromeAutocompleteSchemeClassifier(browser()->profile()));
input.set_keyword_mode_entry_method(metrics::OmniboxEventProto::TAB);
controller()->Start(input);
// Respond for SearchAggregator request.
search_aggregator_response.WaitForRequest();
EXPECT_EQ(search_aggregator_response.http_request()->method,
net::test_server::METHOD_POST);
EXPECT_EQ(
search_aggregator_response.http_request()->content,
base::StringPrintf(R"({"experimentIds":["%s"],)"
R"("query":"john d","suggestionTypes":[1,2,3,5]})",
kEnterpriseSearchAggregatorExperimentId));
search_aggregator_response.Send(net::HTTP_OK, "application/json",
kGoodJsonResponse);
search_aggregator_response.Done();
// Wait for the autocomplete controller to finish.
WaitForAutocompleteDone(browser());
EXPECT_TRUE(controller()->done());
const AutocompleteResult& result = controller()->result();
ASSERT_FALSE(result.empty());
EXPECT_EQ(result.size(), 4u);
EXPECT_THAT(
std::vector<AutocompleteMatch>(result.begin(), result.end()),
testing::ElementsAreArray(std::vector<
testing::Matcher<AutocompleteMatch>>({
AllOf(Field(&AutocompleteMatch::type,
AutocompleteMatchType::SEARCH_OTHER_ENGINE),
Field(&AutocompleteMatch::contents, u"john d"),
Field(&AutocompleteMatch::description, u"Aggregator Search"),
Field(&AutocompleteMatch::destination_url,
GURL("https://www.aggregator.com/search?q=john+d"))),
AllOf(
Field(&AutocompleteMatch::type,
AutocompleteMatchType::SEARCH_SUGGEST),
Field(&AutocompleteMatch::contents, u"John's Demise"),
Field(&AutocompleteMatch::description, u""),
Field(
&AutocompleteMatch::destination_url,
GURL("https://www.aggregator.com/search?q=John%27s+Demise"))),
AllOf(Field(&AutocompleteMatch::type,
AutocompleteMatchType::NAVSUGGEST),
Field(&AutocompleteMatch::contents, u"Aggregator People"),
Field(&AutocompleteMatch::description, u"John Doe"),
Field(&AutocompleteMatch::destination_url,
GURL("https://www.example.com/people/john")),
Field(&AutocompleteMatch::image_url,
GURL("https://example.com/image.png"))),
AllOf(Field(&AutocompleteMatch::type,
AutocompleteMatchType::NAVSUGGEST),
Field(&AutocompleteMatch::contents, u"Jira Issue"),
Field(&AutocompleteMatch::description, u"John's Document"),
Field(&AutocompleteMatch::destination_url,
GURL("https://www.example.com/"))),
})));
}
// TODO(crbug.com/425120649): Flaky.
IN_PROC_BROWSER_TEST_F(OmniboxSearchAggregatorTest,
DISABLED_GoodJsonResponseMultipleRequests) {
std::vector<std::unique_ptr<net::test_server::ControllableHttpResponse>>
requests = {};
for (size_t i = 0; i < 3; ++i) {
requests.push_back(
std::make_unique<net::test_server::ControllableHttpResponse>(
embedded_test_server(), kSearchAggregatorPolicySuggestPath));
}
ASSERT_TRUE(embedded_test_server()->Start());
base::Value policy_value = CreateEnterpriseSearchAggregatorPolicyValue(
embedded_test_server()
->GetURL(kSearchAggregatorPolicySuggestPath)
.spec());
policy::PolicyMap policies;
policies.Set(policy::key::kEnterpriseSearchAggregatorSettings,
policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_USER,
policy::POLICY_SOURCE_CLOUD, std::move(policy_value), nullptr);
policy_provider()->UpdateChromePolicy(policies);
AutocompleteInput input(
kSearchInput, metrics::OmniboxEventProto::NTP,
ChromeAutocompleteSchemeClassifier(browser()->profile()));
input.set_keyword_mode_entry_method(metrics::OmniboxEventProto::TAB);
controller()->Start(input);
std::vector<std::string> request_bodies = {};
for (auto& request : requests) {
request->WaitForRequest();
EXPECT_EQ(request->http_request()->method, net::test_server::METHOD_POST);
request_bodies.push_back(request->http_request()->content);
}
constexpr char expected_request[] =
R"({"experimentIds":["%s"],)"
R"("query":"john d","suggestionTypes":[%s]})";
// Expect unordered because there's no guarantee what order the service starts
// the requests.
EXPECT_THAT(
request_bodies,
testing::UnorderedElementsAre(
base::StringPrintf(expected_request,
kEnterpriseSearchAggregatorExperimentId, "1"),
base::StringPrintf(expected_request,
kEnterpriseSearchAggregatorExperimentId, "2"),
base::StringPrintf(expected_request,
kEnterpriseSearchAggregatorExperimentId, "3,5")));
for (auto& request : requests) {
request->Send(net::HTTP_OK, "application/json", kGoodJsonResponse);
request->Done();
}
// Wait for the autocomplete controller to finish.
WaitForAutocompleteDone(browser());
EXPECT_TRUE(controller()->done());
const AutocompleteResult& result = controller()->result();
ASSERT_FALSE(result.empty());
// When making multiple async requests, only check first match.
EXPECT_THAT(
std::vector<AutocompleteMatch>(result.begin(), result.begin() + 1),
testing::ElementsAreArray(
std::vector<testing::Matcher<AutocompleteMatch>>({
AllOf(
Field(&AutocompleteMatch::type,
AutocompleteMatchType::SEARCH_OTHER_ENGINE),
Field(&AutocompleteMatch::contents, u"john d"),
Field(&AutocompleteMatch::description, u"Aggregator Search"),
Field(&AutocompleteMatch::destination_url,
GURL("https://www.aggregator.com/search?q=john+d"))),
})));
}
// TODO(crbug.com/425120649) Flaky.
IN_PROC_BROWSER_TEST_F(OmniboxSearchAggregatorSingleRequestTest,
DISABLED_RedirectedResponse) {
net::test_server::ControllableHttpResponse redirect_response(
embedded_test_server(), kSearchAggregatorPolicySuggestPath);
const std::string redirected_path = "/suggest-redirect";
net::test_server::ControllableHttpResponse search_aggregator_response(
embedded_test_server(), redirected_path);
ASSERT_TRUE(embedded_test_server()->Start());
base::Value policy_value = CreateEnterpriseSearchAggregatorPolicyValue(
embedded_test_server()
->GetURL(kSearchAggregatorPolicySuggestPath)
.spec());
policy::PolicyMap policies;
policies.Set(policy::key::kEnterpriseSearchAggregatorSettings,
policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_USER,
policy::POLICY_SOURCE_CLOUD, std::move(policy_value), nullptr);
policy_provider()->UpdateChromePolicy(policies);
AutocompleteInput input(
kSearchInput, metrics::OmniboxEventProto::NTP,
ChromeAutocompleteSchemeClassifier(browser()->profile()));
input.set_keyword_mode_entry_method(metrics::OmniboxEventProto::TAB);
controller()->Start(input);
// Redirect response.
redirect_response.WaitForRequest();
EXPECT_EQ(redirect_response.http_request()->method,
net::test_server::METHOD_POST);
redirect_response.Send(net::HTTP_MOVED_PERMANENTLY, "text/html",
/* content = */ "", /* cookies = */ {},
{base::StrCat({"Location: ", redirected_path})});
redirect_response.Done();
// Respond for SearchAggregator request.
search_aggregator_response.WaitForRequest();
EXPECT_EQ(search_aggregator_response.http_request()->method,
net::test_server::METHOD_GET);
search_aggregator_response.Send(net::HTTP_OK, "application/json",
kGoodJsonResponse);
search_aggregator_response.Done();
// Wait for the autocomplete controller to finish.
WaitForAutocompleteDone(browser());
EXPECT_TRUE(controller()->done());
const AutocompleteResult& result = controller()->result();
ASSERT_FALSE(result.empty());
EXPECT_EQ(result.size(), 4u);
}
class OmniboxSearchAggregatorHTTPErrorTest
: public OmniboxSearchAggregatorTest,
public ::testing::WithParamInterface<net::HttpStatusCode> {
public:
net::HttpStatusCode GetHttpStatusCode() { return GetParam(); }
void SetUpOnMainThread() override {
OmniboxSearchAggregatorTest::SetUpOnMainThread();
// Handle search aggregator response
search_aggregator_people_response_ =
std::make_unique<net::test_server::ControllableHttpResponse>(
embedded_test_server(), kSearchAggregatorPolicySuggestPath);
search_aggregator_content_response_ =
std::make_unique<net::test_server::ControllableHttpResponse>(
embedded_test_server(), kSearchAggregatorPolicySuggestPath);
search_aggregator_query_response_ =
std::make_unique<net::test_server::ControllableHttpResponse>(
embedded_test_server(), kSearchAggregatorPolicySuggestPath);
ASSERT_TRUE(embedded_test_server()->Start());
InitSearchAggregatorPolicyConfig();
}
protected:
void InitSearchAggregatorPolicyConfig() {
base::Value policy_value = CreateEnterpriseSearchAggregatorPolicyValue(
embedded_test_server()
->GetURL(kSearchAggregatorPolicySuggestPath)
.spec());
policy::PolicyMap policies;
policies.Set(policy::key::kEnterpriseSearchAggregatorSettings,
policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_USER,
policy::POLICY_SOURCE_CLOUD, std::move(policy_value), nullptr);
policy_provider()->UpdateChromePolicy(policies);
}
net::test_server::ControllableHttpResponse*
search_aggregator_people_response() {
return search_aggregator_people_response_.get();
}
net::test_server::ControllableHttpResponse*
search_aggregator_content_response() {
return search_aggregator_content_response_.get();
}
net::test_server::ControllableHttpResponse*
search_aggregator_query_response() {
return search_aggregator_query_response_.get();
}
private:
std::unique_ptr<net::test_server::ControllableHttpResponse>
search_aggregator_people_response_;
std::unique_ptr<net::test_server::ControllableHttpResponse>
search_aggregator_content_response_;
std::unique_ptr<net::test_server::ControllableHttpResponse>
search_aggregator_query_response_;
};
// TODO(crbug.com/421836646): Flaky on `BUILDFLAG(IS_WIN)`
// TODO(crbug.com/425120649): Flaky.
IN_PROC_BROWSER_TEST_P(OmniboxSearchAggregatorHTTPErrorTest,
DISABLED_HTTPErrorResponse) {
AutocompleteInput input(
kSearchInput, metrics::OmniboxEventProto::NTP,
ChromeAutocompleteSchemeClassifier(browser()->profile()));
input.set_keyword_mode_entry_method(metrics::OmniboxEventProto::TAB);
controller()->Start(input);
// Respond to the first SearchAggregator request (1 - query).
search_aggregator_people_response()->WaitForRequest();
EXPECT_EQ(search_aggregator_people_response()->http_request()->method,
net::test_server::METHOD_POST);
EXPECT_EQ(search_aggregator_people_response()->http_request()->content,
base::StringPrintf(R"({"experimentIds":["%s"],)"
R"("query":"john d","suggestionTypes":[1]})",
kEnterpriseSearchAggregatorExperimentId));
search_aggregator_people_response()->Send(GetHttpStatusCode());
search_aggregator_people_response()->Done();
// Respond to the second SearchAggregator request (2 - people).
search_aggregator_content_response()->WaitForRequest();
EXPECT_EQ(search_aggregator_content_response()->http_request()->method,
net::test_server::METHOD_POST);
EXPECT_EQ(search_aggregator_content_response()->http_request()->content,
base::StringPrintf(R"({"experimentIds":["%s"],)"
R"("query":"john d","suggestionTypes":[2]})",
kEnterpriseSearchAggregatorExperimentId));
search_aggregator_content_response()->Send(GetHttpStatusCode());
search_aggregator_content_response()->Done();
// Respond to the third SearchAggregator request (3,5 - content/Google
// Workspace).
search_aggregator_query_response()->WaitForRequest();
EXPECT_EQ(search_aggregator_query_response()->http_request()->method,
net::test_server::METHOD_POST);
EXPECT_EQ(search_aggregator_query_response()->http_request()->content,
base::StringPrintf(R"({"experimentIds":["%s"],)"
R"("query":"john d","suggestionTypes":[3,5]})",
kEnterpriseSearchAggregatorExperimentId));
search_aggregator_query_response()->Send(GetHttpStatusCode());
search_aggregator_query_response()->Done();
// Wait for the autocomplete controller to finish.
WaitForAutocompleteDone(browser());
EXPECT_TRUE(controller()->done());
const AutocompleteResult& result = controller()->result();
ASSERT_FALSE(result.empty());
EXPECT_EQ(result.size(), 1u);
EXPECT_THAT(
result.default_match(),
AllOf(Field(&AutocompleteMatch::type,
AutocompleteMatchType::SEARCH_OTHER_ENGINE),
Field(&AutocompleteMatch::contents, u"john d"),
Field(&AutocompleteMatch::description, u"Aggregator Search"),
Field(&AutocompleteMatch::destination_url,
GURL("https://www.aggregator.com/search?q=john+d"))));
}
INSTANTIATE_TEST_SUITE_P(
All,
OmniboxSearchAggregatorHTTPErrorTest,
::testing::Values(net::HTTP_BAD_REQUEST,
net::HTTP_FORBIDDEN,
net::HTTP_NOT_FOUND,
net::HTTP_METHOD_NOT_ALLOWED,
net::HTTP_INTERNAL_SERVER_ERROR,
net::HTTP_BAD_GATEWAY),
[](const testing::TestParamInfo<net::HttpStatusCode>& info) {
return base::NumberToString(info.param);
});