blob: 0e55f2658e317a19ac4264ca20dafbc5820fcee9 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/search/search_suggest/search_suggest_loader_impl.h"
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "base/optional.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/mock_callback.h"
#include "base/test/test_simple_task_runner.h"
#include "base/time/time.h"
#include "chrome/browser/search/search_suggest/search_suggest_data.h"
#include "content/public/test/browser_task_environment.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "services/network/test/test_network_connection_tracker.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using testing::_;
using testing::Eq;
using testing::IsEmpty;
using testing::SaveArg;
using testing::StartsWith;
namespace {
const char kApplicationLocale[] = "us";
const char kMinimalValidResponseNoSuggestions[] =
R"json({"update": { "query_suggestions": { "impression_cap_expire_time_ms":
1, "request_freeze_time_ms": 2, "max_impressions": 3}}})json";
const char kMinimalValidResponseWithSuggestions[] =
R"json({"update": { "query_suggestions": {"query_suggestions_with_html":
"<div></div>", "script": "<script></script>","impression_cap_expire_time_ms"
: 1, "request_freeze_time_ms": 2,"max_impressions": 3}}})json";
} // namespace
ACTION_P(Quit, run_loop) {
run_loop->Quit();
}
class SearchSuggestLoaderImplTest : public testing::Test {
public:
SearchSuggestLoaderImplTest()
: SearchSuggestLoaderImplTest(
/*account_consistency_mirror_required=*/false) {}
explicit SearchSuggestLoaderImplTest(bool account_consistency_mirror_required)
: task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP),
test_shared_loader_factory_(
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&test_url_loader_factory_)) {}
void SetUp() override {
testing::Test::SetUp();
search_suggest_loader_ = std::make_unique<SearchSuggestLoaderImpl>(
test_shared_loader_factory_, kApplicationLocale);
}
void SetUpResponseWithData(const std::string& response) {
test_url_loader_factory_.SetInterceptor(base::BindLambdaForTesting(
[&](const network::ResourceRequest& request) {
last_request_url_ = request.url;
last_request_headers_ = request.headers;
}));
test_url_loader_factory_.AddResponse(
search_suggest_loader_->GetLoadURLForTesting().spec(), response);
}
void SetUpResponseWithNetworkError() {
test_url_loader_factory_.AddResponse(
search_suggest_loader_->GetLoadURLForTesting(),
network::mojom::URLResponseHead::New(), std::string(),
network::URLLoaderCompletionStatus(net::HTTP_NOT_FOUND));
}
SearchSuggestLoaderImpl* search_suggest_loader() {
return search_suggest_loader_.get();
}
GURL last_request_url() { return last_request_url_; }
net::HttpRequestHeaders last_request_headers() {
return last_request_headers_;
}
private:
// variations::AppendVariationHeaders requires browser threads.
content::BrowserTaskEnvironment task_environment_;
// Supports JSON parsing in the loader impl.
data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
network::TestURLLoaderFactory test_url_loader_factory_;
scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_;
GURL last_request_url_;
net::HttpRequestHeaders last_request_headers_;
std::unique_ptr<SearchSuggestLoaderImpl> search_suggest_loader_;
};
TEST_F(SearchSuggestLoaderImplTest, RequestReturns) {
SetUpResponseWithData(kMinimalValidResponseWithSuggestions);
base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
std::string blocklist;
search_suggest_loader()->Load(blocklist, callback.Get());
base::Optional<SearchSuggestData> data;
base::RunLoop loop;
EXPECT_CALL(callback,
Run(SearchSuggestLoader::Status::OK_WITH_SUGGESTIONS, _))
.WillOnce(DoAll(SaveArg<1>(&data), Quit(&loop)));
loop.Run();
EXPECT_TRUE(data.has_value());
}
TEST_F(SearchSuggestLoaderImplTest, HandlesResponsePreamble) {
// The response may contain a ")]}'" prefix. The loader should ignore that
// during parsing.
SetUpResponseWithData(std::string(")]}'") +
kMinimalValidResponseWithSuggestions);
base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
std::string blocklist;
search_suggest_loader()->Load(blocklist, callback.Get());
base::Optional<SearchSuggestData> data;
base::RunLoop loop;
EXPECT_CALL(callback,
Run(SearchSuggestLoader::Status::OK_WITH_SUGGESTIONS, _))
.WillOnce(DoAll(SaveArg<1>(&data), Quit(&loop)));
loop.Run();
EXPECT_TRUE(data.has_value());
}
TEST_F(SearchSuggestLoaderImplTest, ParsesFullResponse) {
SetUpResponseWithData(kMinimalValidResponseWithSuggestions);
base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
std::string blocklist;
search_suggest_loader()->Load(blocklist, callback.Get());
base::Optional<SearchSuggestData> data;
base::RunLoop loop;
EXPECT_CALL(callback,
Run(SearchSuggestLoader::Status::OK_WITH_SUGGESTIONS, _))
.WillOnce(DoAll(SaveArg<1>(&data), Quit(&loop)));
loop.Run();
ASSERT_TRUE(data.has_value());
EXPECT_EQ("<div></div>", data->suggestions_html);
EXPECT_EQ("<script></script>", data->end_of_body_script);
EXPECT_EQ(1, data->impression_cap_expire_time_ms);
EXPECT_EQ(2, data->request_freeze_time_ms);
EXPECT_EQ(3, data->max_impressions);
}
TEST_F(SearchSuggestLoaderImplTest, ParsesValidResponseWithNoSuggestions) {
SetUpResponseWithData(kMinimalValidResponseNoSuggestions);
base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
std::string blocklist;
search_suggest_loader()->Load(blocklist, callback.Get());
base::Optional<SearchSuggestData> data;
base::RunLoop loop;
EXPECT_CALL(callback,
Run(SearchSuggestLoader::Status::OK_WITHOUT_SUGGESTIONS, _))
.WillOnce(DoAll(SaveArg<1>(&data), Quit(&loop)));
loop.Run();
ASSERT_TRUE(data.has_value());
EXPECT_EQ(std::string(), data->suggestions_html);
EXPECT_EQ(std::string(), data->end_of_body_script);
EXPECT_EQ(1, data->impression_cap_expire_time_ms);
EXPECT_EQ(2, data->request_freeze_time_ms);
EXPECT_EQ(3, data->max_impressions);
}
TEST_F(SearchSuggestLoaderImplTest, CoalescesMultipleRequests) {
SetUpResponseWithData(kMinimalValidResponseWithSuggestions);
// Trigger two requests.
base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback>
first_callback;
std::string blocklist;
search_suggest_loader()->Load(blocklist, first_callback.Get());
base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback>
second_callback;
search_suggest_loader()->Load(blocklist, second_callback.Get());
// Make sure that a single response causes both callbacks to be called.
base::Optional<SearchSuggestData> first_data;
base::Optional<SearchSuggestData> second_data;
base::RunLoop loop;
EXPECT_CALL(first_callback,
Run(SearchSuggestLoader::Status::OK_WITH_SUGGESTIONS, _))
.WillOnce(SaveArg<1>(&first_data));
EXPECT_CALL(second_callback,
Run(SearchSuggestLoader::Status::OK_WITH_SUGGESTIONS, _))
.WillOnce(DoAll(SaveArg<1>(&second_data), Quit(&loop)));
loop.Run();
// Ensure that both requests received a response.
EXPECT_TRUE(first_data.has_value());
EXPECT_TRUE(second_data.has_value());
}
TEST_F(SearchSuggestLoaderImplTest, NetworkErrorIsTransient) {
SetUpResponseWithNetworkError();
base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
std::string blocklist;
search_suggest_loader()->Load(blocklist, callback.Get());
base::RunLoop loop;
EXPECT_CALL(callback, Run(SearchSuggestLoader::Status::TRANSIENT_ERROR,
Eq(base::nullopt)))
.WillOnce(Quit(&loop));
loop.Run();
}
// Flaky, see https://crbug.com/923953.
TEST_F(SearchSuggestLoaderImplTest, DISABLED_InvalidJsonErrorIsFatal) {
SetUpResponseWithData(kMinimalValidResponseWithSuggestions +
std::string(")"));
base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
std::string blocklist;
search_suggest_loader()->Load(blocklist, callback.Get());
base::RunLoop loop;
EXPECT_CALL(callback,
Run(SearchSuggestLoader::Status::FATAL_ERROR, Eq(base::nullopt)))
.WillOnce(Quit(&loop));
loop.Run();
}
TEST_F(SearchSuggestLoaderImplTest, IncompleteJsonErrorIsFatal) {
SetUpResponseWithData(R"json({"update": {}})json");
base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
std::string blocklist;
search_suggest_loader()->Load(blocklist, callback.Get());
base::RunLoop loop;
EXPECT_CALL(callback,
Run(SearchSuggestLoader::Status::FATAL_ERROR, Eq(base::nullopt)))
.WillOnce(Quit(&loop));
loop.Run();
}