blob: 9e201043e686b59bf49e9ca5f9afb8aa79c3b952 [file] [log] [blame]
// Copyright 2017 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/ui/omnibox/chrome_omnibox_navigation_observer.h"
#include <unordered_map>
#include <vector>
#include "base/run_loop.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/infobars/infobar_service.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/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/notification_details.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_source.h"
#include "content/public/browser/notification_types.h"
#include "content/public/browser/web_contents.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/url_request/test_url_fetcher_factory.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_fetcher_factory.h"
#include "net/url_request/url_request_status.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "services/network/test/test_utils.h"
using network::TestURLLoaderFactory;
// A trival ChromeOmniboxNavigationObserver that keeps track of whether
// CreateAlternateNavInfoBar() has been called.
class MockChromeOmniboxNavigationObserver
: public ChromeOmniboxNavigationObserver {
public:
MockChromeOmniboxNavigationObserver(
Profile* profile,
const base::string16& text,
const AutocompleteMatch& match,
const AutocompleteMatch& alternate_nav_match,
bool* displayed_infobar)
: ChromeOmniboxNavigationObserver(profile,
text,
match,
alternate_nav_match),
displayed_infobar_(displayed_infobar) {
*displayed_infobar_ = false;
}
protected:
void CreateAlternateNavInfoBar() override { *displayed_infobar_ = true; }
private:
// True if CreateAlternateNavInfoBar was called. This cannot be kept in
// memory within this class because this class is automatically deleted when
// all fetchers finish (before the test can query this value), hence the
// pointer.
bool* displayed_infobar_;
DISALLOW_COPY_AND_ASSIGN(MockChromeOmniboxNavigationObserver);
};
class ChromeOmniboxNavigationObserverTest
: public ChromeRenderViewHostTestHarness {
protected:
ChromeOmniboxNavigationObserverTest() {}
~ChromeOmniboxNavigationObserverTest() override {}
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 base::string16 auto_generated_search_keyword() {
return base::ASCIIToUTF16("auto_generated_search_keyword");
}
static base::string16 non_auto_generated_search_keyword() {
return base::ASCIIToUTF16("non_auto_generated_search_keyword");
}
static base::string16 default_search_keyword() {
return base::ASCIIToUTF16("default_search_keyword");
}
static base::string16 prepopulated_search_keyword() {
return base::ASCIIToUTF16("prepopulated_search_keyword");
}
static base::string16 policy_search_keyword() {
return base::ASCIIToUTF16("policy_search_keyword");
}
private:
// ChromeRenderViewHostTestHarness:
void SetUp() override;
DISALLOW_COPY_AND_ASSIGN(ChromeOmniboxNavigationObserverTest);
};
void ChromeOmniboxNavigationObserverTest::SetUp() {
ChromeRenderViewHostTestHarness::SetUp();
InfoBarService::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.created_by_policy = true;
factory_util.model()->Add(std::make_unique<TemplateURL>(policy_turl));
}
TEST_F(ChromeOmniboxNavigationObserverTest, LoadStateAfterPendingNavigation) {
std::unique_ptr<ChromeOmniboxNavigationObserver> observer =
std::make_unique<ChromeOmniboxNavigationObserver>(
profile(), base::ASCIIToUTF16("test text"), AutocompleteMatch(),
AutocompleteMatch());
EXPECT_EQ(ChromeOmniboxNavigationObserver::LOAD_NOT_SEEN,
observer->load_state());
std::unique_ptr<content::NavigationEntry> entry =
content::NavigationController::CreateNavigationEntry(
GURL(), content::Referrer(), ui::PAGE_TRANSITION_FROM_ADDRESS_BAR,
false, std::string(), profile(),
nullptr /* blob_url_loader_factory */);
content::NotificationService::current()->Notify(
content::NOTIFICATION_NAV_ENTRY_PENDING,
content::Source<content::NavigationController>(navigation_controller()),
content::Details<content::NavigationEntry>(entry.get()));
// A pending navigation notification should synchronously update the load
// state to pending.
EXPECT_EQ(ChromeOmniboxNavigationObserver::LOAD_PENDING,
observer->load_state());
}
TEST_F(ChromeOmniboxNavigationObserverTest, DeleteBrokenCustomSearchEngines) {
struct TestData {
base::string16 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}};
base::string16 query = base::ASCIIToUTF16(" 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) != nullptr);
AutocompleteMatch match;
match.keyword = cases[i].keyword;
// |observer| gets deleted by observer->NavigationEntryCommitted().
ChromeOmniboxNavigationObserver* observer =
new ChromeOmniboxNavigationObserver(profile(), cases[i].keyword + query,
match, AutocompleteMatch());
auto navigation_entry =
content::NavigationController::CreateNavigationEntry(
GURL(), content::Referrer(), ui::PAGE_TRANSITION_FROM_ADDRESS_BAR,
false, std::string(), profile(),
nullptr /* blob_url_loader_factory */);
content::LoadCommittedDetails details;
details.http_status_code = cases[i].status_code;
details.entry = navigation_entry.get();
observer->NavigationEntryCommitted(details);
EXPECT_EQ(cases[i].expect_exists,
model()->GetTemplateURLForKeyword(cases[i].keyword) != nullptr);
}
// Also run a URL navigation that results in a 404 through the system to make
// sure nothing crashes for regular URL navigations.
// |observer| gets deleted by observer->NavigationEntryCommitted().
ChromeOmniboxNavigationObserver* observer =
new ChromeOmniboxNavigationObserver(
profile(), base::ASCIIToUTF16("url navigation"), AutocompleteMatch(),
AutocompleteMatch());
auto navigation_entry = content::NavigationController::CreateNavigationEntry(
GURL(), content::Referrer(), ui::PAGE_TRANSITION_FROM_ADDRESS_BAR, false,
std::string(), profile(), nullptr /* blob_url_loader_factory */);
content::LoadCommittedDetails details;
details.http_status_code = 404;
details.entry = navigation_entry.get();
observer->NavigationEntryCommitted(details);
}
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 = 0;
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;
} cases[] = {
// 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 < base::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;
network::ResourceResponseHead redir_head =
network::CreateResourceResponseHead(net::HTTP_MOVED_PERMANENTLY);
redirects.push_back({redir_info, redir_head});
}
// Fill in final response.
network::ResourceResponseHead http_head;
network::URLLoaderCompletionStatus net_status;
network::TestURLLoaderFactory::ResponseProduceFlags response_flags =
network::TestURLLoaderFactory::kResponseDefault;
if (response.http_response_code == kNoResponse) {
response_flags =
TestURLLoaderFactory::kResponseOnlyRedirectsNoDestination;
} else if (response.http_response_code == kNetError) {
net_status = network::URLLoaderCompletionStatus(net::ERR_FAILED);
} else {
net_status = network::URLLoaderCompletionStatus(net::OK);
http_head = network::CreateResourceResponseHead(
static_cast<net::HttpStatusCode>(response.http_response_code));
}
test_url_loader_factory.AddResponse(GURL(response.urls[0]), http_head,
response.content, net_status,
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/");
bool displayed_infobar;
ChromeOmniboxNavigationObserver* observer =
new MockChromeOmniboxNavigationObserver(
profile(), base::ASCIIToUTF16("example"), AutocompleteMatch(),
alternate_nav_match, &displayed_infobar);
observer->SetURLLoaderFactoryForTesting(shared_factory);
// Send the observer NAV_ENTRY_PENDING to get the URL fetcher to start.
auto navigation_entry =
content::NavigationController::CreateNavigationEntry(
GURL(), content::Referrer(), ui::PAGE_TRANSITION_FROM_ADDRESS_BAR,
false, std::string(), profile(),
nullptr /* blob_url_loader_factory */);
content::NotificationService::current()->Notify(
content::NOTIFICATION_NAV_ENTRY_PENDING,
content::Source<content::NavigationController>(navigation_controller()),
content::Details<content::NavigationEntry>(navigation_entry.get()));
// Make sure the fetcher(s) have finished.
base::RunLoop().RunUntilIdle();
// Send the observer NavigationEntryCommitted() to get it to display the
// infobar if needed.
content::LoadCommittedDetails details;
details.http_status_code = 200;
details.entry = navigation_entry.get();
observer->NavigationEntryCommitted(details);
// See if AlternateNavInfoBarDelegate::Create() was called.
EXPECT_EQ(test_case.expected_alternate_nav_bar_shown, displayed_infobar);
}
}