| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/omnibox/chrome_omnibox_navigation_observer.h" |
| |
| #include <array> |
| #include <unordered_map> |
| #include <vector> |
| |
| #include "base/run_loop.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/bind.h" |
| #include "chrome/browser/search_engines/template_url_service_factory.h" |
| #include "chrome/browser/search_engines/template_url_service_factory_test_util.h" |
| #include "chrome/test/base/chrome_render_view_host_test_harness.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "components/infobars/content/content_infobar_manager.h" |
| #include "components/search_engines/template_url.h" |
| #include "components/search_engines/template_url_data.h" |
| #include "components/search_engines/template_url_service.h" |
| #include "content/public/browser/navigation_details.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/test/navigation_simulator.h" |
| #include "net/http/http_response_headers.h" |
| #include "net/http/http_status_code.h" |
| #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "services/network/test/test_url_loader_factory.h" |
| #include "services/network/test/test_utils.h" |
| |
| using network::TestURLLoaderFactory; |
| |
| class ChromeOmniboxNavigationObserverTest |
| : public ChromeRenderViewHostTestHarness { |
| public: |
| ChromeOmniboxNavigationObserverTest( |
| const ChromeOmniboxNavigationObserverTest&) = delete; |
| ChromeOmniboxNavigationObserverTest& operator=( |
| const ChromeOmniboxNavigationObserverTest&) = delete; |
| |
| protected: |
| ChromeOmniboxNavigationObserverTest() = default; |
| ~ChromeOmniboxNavigationObserverTest() override = default; |
| |
| content::NavigationController* navigation_controller() { |
| return &(web_contents()->GetController()); |
| } |
| |
| TemplateURLService* model() { |
| return TemplateURLServiceFactory::GetForProfile(profile()); |
| } |
| |
| // Functions that return the name of certain search keywords that are part |
| // of the TemplateURLService attached to this profile. |
| static std::u16string auto_generated_search_keyword() { |
| return u"auto_generated_search_keyword"; |
| } |
| static std::u16string non_auto_generated_search_keyword() { |
| return u"non_auto_generated_search_keyword"; |
| } |
| static std::u16string default_search_keyword() { |
| return u"default_search_keyword"; |
| } |
| static std::u16string prepopulated_search_keyword() { |
| return u"prepopulated_search_keyword"; |
| } |
| static std::u16string policy_search_keyword() { |
| return u"policy_search_keyword"; |
| } |
| static std::u16string starter_pack_keyword() { |
| return u"starter_pack_keyword"; |
| } |
| |
| private: |
| // ChromeRenderViewHostTestHarness: |
| void SetUp() override; |
| }; |
| |
| void ChromeOmniboxNavigationObserverTest::SetUp() { |
| ChromeRenderViewHostTestHarness::SetUp(); |
| infobars::ContentInfoBarManager::CreateForWebContents(web_contents()); |
| |
| // Set up a series of search engines for later testing. |
| TemplateURLServiceFactoryTestUtil factory_util(profile()); |
| factory_util.VerifyLoad(); |
| |
| TemplateURLData auto_gen_turl; |
| auto_gen_turl.SetKeyword(auto_generated_search_keyword()); |
| auto_gen_turl.safe_for_autoreplace = true; |
| factory_util.model()->Add(std::make_unique<TemplateURL>(auto_gen_turl)); |
| |
| TemplateURLData non_auto_gen_turl; |
| non_auto_gen_turl.SetKeyword(non_auto_generated_search_keyword()); |
| factory_util.model()->Add(std::make_unique<TemplateURL>(non_auto_gen_turl)); |
| |
| TemplateURLData default_turl; |
| default_turl.SetKeyword(default_search_keyword()); |
| factory_util.model()->SetUserSelectedDefaultSearchProvider( |
| factory_util.model()->Add(std::make_unique<TemplateURL>(default_turl))); |
| |
| TemplateURLData prepopulated_turl; |
| prepopulated_turl.SetKeyword(prepopulated_search_keyword()); |
| prepopulated_turl.prepopulate_id = 1; |
| factory_util.model()->Add(std::make_unique<TemplateURL>(prepopulated_turl)); |
| |
| TemplateURLData policy_turl; |
| policy_turl.SetKeyword(policy_search_keyword()); |
| policy_turl.policy_origin = |
| TemplateURLData::PolicyOrigin::kDefaultSearchProvider; |
| factory_util.model()->Add(std::make_unique<TemplateURL>(policy_turl)); |
| |
| TemplateURLData starter_pack_turl; |
| starter_pack_turl.SetKeyword(starter_pack_keyword()); |
| starter_pack_turl.starter_pack_id = 1; |
| factory_util.model()->Add(std::make_unique<TemplateURL>(starter_pack_turl)); |
| } |
| |
| namespace { |
| |
| scoped_refptr<net::HttpResponseHeaders> GetHeadersForResponseCode(int code) { |
| if (code == 200) { |
| return base::MakeRefCounted<net::HttpResponseHeaders>( |
| "HTTP/1.1 200 OK\r\n"); |
| } else if (code == 404) { |
| return base::MakeRefCounted<net::HttpResponseHeaders>( |
| "HTTP/1.1 404 Not Found\r\n"); |
| } |
| NOTREACHED(); |
| } |
| |
| void WriteMojoMessage(const mojo::ScopedDataPipeProducerHandle& handle, |
| std::string message) { |
| size_t actually_written_bytes = 0; |
| ASSERT_EQ(MOJO_RESULT_OK, handle->WriteData(base::as_byte_span(message), |
| MOJO_WRITE_DATA_FLAG_NONE, |
| actually_written_bytes)); |
| ASSERT_EQ(message.size(), actually_written_bytes); |
| } |
| |
| } // namespace |
| |
| TEST_F(ChromeOmniboxNavigationObserverTest, DeleteBrokenCustomSearchEngines) { |
| // The actual URL doesn't matter for this test as long as it's valid. |
| struct TestData { |
| std::u16string keyword; |
| int status_code; |
| bool expect_exists; |
| }; |
| std::vector<TestData> cases = { |
| {auto_generated_search_keyword(), 200, true}, |
| {auto_generated_search_keyword(), 404, false}, |
| {non_auto_generated_search_keyword(), 404, true}, |
| {default_search_keyword(), 404, true}, |
| {prepopulated_search_keyword(), 404, true}, |
| {policy_search_keyword(), 404, true}, |
| {starter_pack_keyword(), 404, true}}; |
| |
| std::u16string query = u" text"; |
| for (size_t i = 0; i < cases.size(); ++i) { |
| SCOPED_TRACE("case #" + base::NumberToString(i)); |
| // The keyword should always exist at the beginning. |
| EXPECT_TRUE(model()->GetTemplateURLForKeyword(cases[i].keyword)); |
| |
| AutocompleteMatch match; |
| match.keyword = cases[i].keyword; |
| // Append the case number to the URL to ensure that we are loading a new |
| // page. |
| auto navigation = content::NavigationSimulator::CreateBrowserInitiated( |
| GURL(base::StringPrintf("https://foo.com/%zu", i)), web_contents()); |
| navigation->SetResponseHeaders( |
| GetHeadersForResponseCode(cases[i].status_code)); |
| |
| // HTTPErrorNavigationThrottle checks whether body is null or not to decide |
| // whether to commit an error page or a regular one. |
| mojo::ScopedDataPipeProducerHandle producer_handle; |
| mojo::ScopedDataPipeConsumerHandle consumer_handle; |
| ASSERT_EQ(mojo::CreateDataPipe(nullptr, producer_handle, consumer_handle), |
| MOJO_RESULT_OK); |
| navigation->SetResponseBody(std::move(consumer_handle)); |
| WriteMojoMessage(producer_handle, "data"); |
| |
| navigation->Start(); |
| ChromeOmniboxNavigationObserver::CreateForTesting( |
| navigation->GetNavigationHandle(), profile(), cases[i].keyword + query, |
| match, AutocompleteMatch(), nullptr, base::DoNothing()); |
| |
| navigation->Commit(); |
| |
| EXPECT_EQ(cases[i].expect_exists, |
| model()->GetTemplateURLForKeyword(cases[i].keyword) != nullptr); |
| } |
| } |
| |
| TEST_F(ChromeOmniboxNavigationObserverTest, AlternateNavInfoBar) { |
| TestURLLoaderFactory test_url_loader_factory; |
| scoped_refptr<network::SharedURLLoaderFactory> shared_factory = |
| base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>( |
| &test_url_loader_factory); |
| |
| const int kNetError = net::ERR_FAILED; |
| const int kNoResponse = -1; |
| struct Response { |
| const std::vector<std::string> urls; // If more than one, 301 between them. |
| // The final status code to return after all the redirects, or one of |
| // kNetError or kNoResponse. |
| const int http_response_code; |
| std::string content; |
| }; |
| |
| // All of these test cases assume the alternate nav URL is http://example/. |
| struct Case { |
| const Response response; |
| const bool expected_alternate_nav_bar_shown; |
| }; |
| auto cases = std::to_array<Case>({ |
| // The only response provided is a net error. |
| {{{"http://example/"}, kNetError}, false}, |
| // The response connected to a valid page. |
| {{{"http://example/"}, 200}, true}, |
| // A non-empty page, despite the HEAD. |
| {{{"http://example/"}, 200, "Content"}, true}, |
| // The response connected to an error page. |
| {{{"http://example/"}, 404}, false}, |
| |
| // The response redirected to same host, just http->https, with a path |
| // change as well. In this case the second URL should not fetched; Chrome |
| // will optimistically assume the destination will return a valid page and |
| // display the infobar. |
| {{{"http://example/", "https://example/path"}, kNoResponse}, true}, |
| // Ditto, making sure it still holds when the final destination URL |
| // returns a valid status code. |
| {{{"http://example/", "https://example/path"}, 200}, true}, |
| |
| // The response redirected to an entirely different host. In these cases, |
| // no URL should be fetched against this second host; again Chrome will |
| // optimistically assume the destination will return a valid page and |
| // display the infobar. |
| {{{"http://example/", "http://new-destination/"}, kNoResponse}, true}, |
| // Ditto, making sure it still holds when the final destination URL |
| // returns a valid status code. |
| {{{"http://example/", "http://new-destination/"}, 200}, true}, |
| |
| // The response redirected to same host, just http->https, with no other |
| // changes. In these cases, Chrome will fetch the second URL. |
| // The second URL response returned a valid page. |
| {{{"http://example/", "https://example/"}, 200}, true}, |
| // The second URL response returned an error page. |
| {{{"http://example/", "https://example/"}, 404}, false}, |
| // The second URL response returned a net error. |
| {{{"http://example/", "https://example/"}, kNetError}, false}, |
| // The second URL response redirected again. |
| {{{"http://example/", "https://example/", "https://example/root"}, |
| kNoResponse}, |
| true}, |
| }); |
| for (size_t i = 0; i < std::size(cases); ++i) { |
| SCOPED_TRACE("case #" + base::NumberToString(i)); |
| const Case& test_case = cases[i]; |
| const Response& response = test_case.response; |
| |
| // Set the URL request responses. |
| test_url_loader_factory.ClearResponses(); |
| |
| // Compute URL redirect chain. |
| TestURLLoaderFactory::Redirects redirects; |
| for (size_t dest = 1; dest < response.urls.size(); ++dest) { |
| net::RedirectInfo redir_info; |
| redir_info.new_url = GURL(response.urls[dest]); |
| redir_info.status_code = net::HTTP_MOVED_PERMANENTLY; |
| auto redir_head = |
| network::CreateURLResponseHead(net::HTTP_MOVED_PERMANENTLY); |
| redirects.emplace_back(redir_info, std::move(redir_head)); |
| } |
| |
| // Fill in final response. |
| network::mojom::URLResponseHeadPtr http_head = |
| network::mojom::URLResponseHead::New(); |
| network::URLLoaderCompletionStatus net_status; |
| if (response.http_response_code == kNetError) { |
| net_status = network::URLLoaderCompletionStatus(net::ERR_FAILED); |
| } else if (response.http_response_code != kNoResponse) { |
| net_status = network::URLLoaderCompletionStatus(net::OK); |
| http_head = network::CreateURLResponseHead( |
| static_cast<net::HttpStatusCode>(response.http_response_code)); |
| } |
| |
| test_url_loader_factory.AddResponse(GURL(response.urls[0]), |
| std::move(http_head), response.content, |
| net_status, std::move(redirects)); |
| |
| // Create the alternate nav match and the observer. |
| // |observer| gets deleted automatically after all fetchers complete. |
| AutocompleteMatch alternate_nav_match; |
| alternate_nav_match.destination_url = GURL("http://example/"); |
| auto navigation = content::NavigationSimulator::CreateBrowserInitiated( |
| GURL(base::StringPrintf("https://foo.com/%zu", i)), web_contents()); |
| bool displayed_infobar = false; |
| |
| navigation->Start(); |
| ChromeOmniboxNavigationObserver::CreateForTesting( |
| navigation->GetNavigationHandle(), profile(), u"example", |
| AutocompleteMatch(), alternate_nav_match, shared_factory.get(), |
| base::BindLambdaForTesting( |
| [&](ChromeOmniboxNavigationObserver* observer) { |
| displayed_infobar = true; |
| })); |
| |
| // Make sure the fetcher(s) have finished. |
| base::RunLoop().RunUntilIdle(); |
| |
| if (test_case.response.http_response_code != kNetError) { |
| navigation->Commit(); |
| } else { |
| navigation->Fail(kNetError); |
| navigation->CommitErrorPage(); |
| } |
| |
| // See if AlternateNavInfoBarDelegate::Create() was called. |
| EXPECT_EQ(test_case.expected_alternate_nav_bar_shown, displayed_infobar); |
| } |
| } |