blob: fa810dfa251cbdbb6cdd48a1e5cd13b3822394e9 [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_test_util.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 "components/google/core/browser/google_url_tracker.h"
#include "components/signin/core/browser/signin_header_helper.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "content/public/test/test_service_manager_context.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "services/data_decoder/public/cpp/testing_json_parser.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 kMinimalValidResponse[] = R"json({"update": { "search_suggestions":""
}})json";
// Required to instantiate a GoogleUrlTracker in UNIT_TEST_MODE.
class GoogleURLTrackerClientStub : public GoogleURLTrackerClient {
public:
GoogleURLTrackerClientStub() {}
~GoogleURLTrackerClientStub() override {}
bool IsBackgroundNetworkingEnabled() override { return true; }
PrefService* GetPrefs() override { return nullptr; }
network::SharedURLLoaderFactory* GetURLLoaderFactory() override {
return nullptr;
}
private:
DISALLOW_COPY_AND_ASSIGN(GoogleURLTrackerClientStub);
};
} // 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)
: thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP),
google_url_tracker_(
std::make_unique<GoogleURLTrackerClientStub>(),
GoogleURLTracker::ALWAYS_DOT_COM_MODE,
network::TestNetworkConnectionTracker::GetInstance()),
test_shared_loader_factory_(
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&test_url_loader_factory_)) {}
~SearchSuggestLoaderImplTest() override {
static_cast<KeyedService&>(google_url_tracker_).Shutdown();
}
void SetUp() override {
testing::Test::SetUp();
search_suggest_loader_ = std::make_unique<SearchSuggestLoaderImpl>(
test_shared_loader_factory_, &google_url_tracker_, 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::ResourceResponseHead(), 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 and SafeJsonParser require a
// thread and a ServiceManagerConnection to be set.
content::TestBrowserThreadBundle thread_bundle_;
content::TestServiceManagerContext service_manager_context_;
data_decoder::TestingJsonParser::ScopedFactoryOverride factory_override_;
GoogleURLTracker google_url_tracker_;
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(kMinimalValidResponse);
base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
search_suggest_loader()->Load(callback.Get());
base::Optional<SearchSuggestData> data;
base::RunLoop loop;
EXPECT_CALL(callback, Run(SearchSuggestLoader::Status::OK, _))
.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(")]}'") + kMinimalValidResponse);
base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
search_suggest_loader()->Load(callback.Get());
base::Optional<SearchSuggestData> data;
base::RunLoop loop;
EXPECT_CALL(callback, Run(SearchSuggestLoader::Status::OK, _))
.WillOnce(DoAll(SaveArg<1>(&data), Quit(&loop)));
loop.Run();
EXPECT_TRUE(data.has_value());
}
TEST_F(SearchSuggestLoaderImplTest, ParsesFullResponse) {
SetUpResponseWithData(
R"json({"update": { "search_suggestions" : "<div></div>"}})json");
base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
search_suggest_loader()->Load(callback.Get());
base::Optional<SearchSuggestData> data;
base::RunLoop loop;
EXPECT_CALL(callback, Run(SearchSuggestLoader::Status::OK, _))
.WillOnce(DoAll(SaveArg<1>(&data), Quit(&loop)));
loop.Run();
ASSERT_TRUE(data.has_value());
EXPECT_THAT(data->suggestions_html, Eq("<div></div>"));
}
TEST_F(SearchSuggestLoaderImplTest, CoalescesMultipleRequests) {
SetUpResponseWithData(kMinimalValidResponse);
// Trigger two requests.
base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback>
first_callback;
search_suggest_loader()->Load(first_callback.Get());
base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback>
second_callback;
search_suggest_loader()->Load(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, _))
.WillOnce(SaveArg<1>(&first_data));
EXPECT_CALL(second_callback, Run(SearchSuggestLoader::Status::OK, _))
.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;
search_suggest_loader()->Load(callback.Get());
base::RunLoop loop;
EXPECT_CALL(callback, Run(SearchSuggestLoader::Status::TRANSIENT_ERROR,
Eq(base::nullopt)))
.WillOnce(Quit(&loop));
loop.Run();
}
TEST_F(SearchSuggestLoaderImplTest, InvalidJsonErrorIsFatal) {
SetUpResponseWithData(kMinimalValidResponse + std::string(")"));
base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
search_suggest_loader()->Load(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;
search_suggest_loader()->Load(callback.Get());
base::RunLoop loop;
EXPECT_CALL(callback,
Run(SearchSuggestLoader::Status::FATAL_ERROR, Eq(base::nullopt)))
.WillOnce(Quit(&loop));
loop.Run();
}