| // 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(); |
| } |