1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/ui/omnibox/chrome_omnibox_navigation_observer.h"
6
7 #include <unordered_map>
8 #include <vector>
9
10 #include "base/run_loop.h"
11 #include "base/stl_util.h"
12 #include "base/strings/string_number_conversions.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "chrome/browser/infobars/infobar_service.h"
15 #include "chrome/browser/search_engines/template_url_service_factory.h"
16 #include "chrome/browser/search_engines/template_url_service_factory_test_util.h"
17 #include "chrome/test/base/chrome_render_view_host_test_harness.h"
18 #include "chrome/test/base/testing_profile.h"
19 #include "components/search_engines/template_url.h"
20 #include "components/search_engines/template_url_data.h"
21 #include "components/search_engines/template_url_service.h"
22 #include "content/public/browser/navigation_details.h"
23 #include "content/public/browser/navigation_entry.h"
24 #include "content/public/browser/notification_details.h"
25 #include "content/public/browser/notification_service.h"
26 #include "content/public/browser/notification_source.h"
27 #include "content/public/browser/notification_types.h"
28 #include "content/public/browser/web_contents.h"
29 #include "net/http/http_response_headers.h"
30 #include "net/http/http_status_code.h"
31 #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
32 #include "services/network/test/test_url_loader_factory.h"
33 #include "services/network/test/test_utils.h"
34
35 using network::TestURLLoaderFactory;
36
37 // A trival ChromeOmniboxNavigationObserver that keeps track of whether
38 // CreateAlternateNavInfoBar() has been called.
39 class MockChromeOmniboxNavigationObserver
40 : public ChromeOmniboxNavigationObserver {
41 public:
MockChromeOmniboxNavigationObserver(Profile * profile,const base::string16 & text,const AutocompleteMatch & match,const AutocompleteMatch & alternate_nav_match,bool * displayed_infobar)42 MockChromeOmniboxNavigationObserver(
43 Profile* profile,
44 const base::string16& text,
45 const AutocompleteMatch& match,
46 const AutocompleteMatch& alternate_nav_match,
47 bool* displayed_infobar)
48 : ChromeOmniboxNavigationObserver(profile,
49 text,
50 match,
51 alternate_nav_match),
52 displayed_infobar_(displayed_infobar) {
53 *displayed_infobar_ = false;
54 }
55
56 protected:
CreateAlternateNavInfoBar()57 void CreateAlternateNavInfoBar() override { *displayed_infobar_ = true; }
58
59 private:
60 // True if CreateAlternateNavInfoBar was called. This cannot be kept in
61 // memory within this class because this class is automatically deleted when
62 // all fetchers finish (before the test can query this value), hence the
63 // pointer.
64 bool* displayed_infobar_;
65
66 DISALLOW_COPY_AND_ASSIGN(MockChromeOmniboxNavigationObserver);
67 };
68
69 class ChromeOmniboxNavigationObserverTest
70 : public ChromeRenderViewHostTestHarness {
71 protected:
ChromeOmniboxNavigationObserverTest()72 ChromeOmniboxNavigationObserverTest() {}
~ChromeOmniboxNavigationObserverTest()73 ~ChromeOmniboxNavigationObserverTest() override {}
74
navigation_controller()75 content::NavigationController* navigation_controller() {
76 return &(web_contents()->GetController());
77 }
78
model()79 TemplateURLService* model() {
80 return TemplateURLServiceFactory::GetForProfile(profile());
81 }
82
83 // Functions that return the name of certain search keywords that are part
84 // of the TemplateURLService attached to this profile.
auto_generated_search_keyword()85 static base::string16 auto_generated_search_keyword() {
86 return base::ASCIIToUTF16("auto_generated_search_keyword");
87 }
non_auto_generated_search_keyword()88 static base::string16 non_auto_generated_search_keyword() {
89 return base::ASCIIToUTF16("non_auto_generated_search_keyword");
90 }
default_search_keyword()91 static base::string16 default_search_keyword() {
92 return base::ASCIIToUTF16("default_search_keyword");
93 }
prepopulated_search_keyword()94 static base::string16 prepopulated_search_keyword() {
95 return base::ASCIIToUTF16("prepopulated_search_keyword");
96 }
policy_search_keyword()97 static base::string16 policy_search_keyword() {
98 return base::ASCIIToUTF16("policy_search_keyword");
99 }
100
101 private:
102 // ChromeRenderViewHostTestHarness:
103 void SetUp() override;
104
105 DISALLOW_COPY_AND_ASSIGN(ChromeOmniboxNavigationObserverTest);
106 };
107
SetUp()108 void ChromeOmniboxNavigationObserverTest::SetUp() {
109 ChromeRenderViewHostTestHarness::SetUp();
110 InfoBarService::CreateForWebContents(web_contents());
111
112 // Set up a series of search engines for later testing.
113 TemplateURLServiceFactoryTestUtil factory_util(profile());
114 factory_util.VerifyLoad();
115
116 TemplateURLData auto_gen_turl;
117 auto_gen_turl.SetKeyword(auto_generated_search_keyword());
118 auto_gen_turl.safe_for_autoreplace = true;
119 factory_util.model()->Add(std::make_unique<TemplateURL>(auto_gen_turl));
120
121 TemplateURLData non_auto_gen_turl;
122 non_auto_gen_turl.SetKeyword(non_auto_generated_search_keyword());
123 factory_util.model()->Add(std::make_unique<TemplateURL>(non_auto_gen_turl));
124
125 TemplateURLData default_turl;
126 default_turl.SetKeyword(default_search_keyword());
127 factory_util.model()->SetUserSelectedDefaultSearchProvider(
128 factory_util.model()->Add(std::make_unique<TemplateURL>(default_turl)));
129
130 TemplateURLData prepopulated_turl;
131 prepopulated_turl.SetKeyword(prepopulated_search_keyword());
132 prepopulated_turl.prepopulate_id = 1;
133 factory_util.model()->Add(std::make_unique<TemplateURL>(prepopulated_turl));
134
135 TemplateURLData policy_turl;
136 policy_turl.SetKeyword(policy_search_keyword());
137 policy_turl.created_by_policy = true;
138 factory_util.model()->Add(std::make_unique<TemplateURL>(policy_turl));
139 }
140
TEST_F(ChromeOmniboxNavigationObserverTest,LoadStateAfterPendingNavigation)141 TEST_F(ChromeOmniboxNavigationObserverTest, LoadStateAfterPendingNavigation) {
142 std::unique_ptr<ChromeOmniboxNavigationObserver> observer =
143 std::make_unique<ChromeOmniboxNavigationObserver>(
144 profile(), base::ASCIIToUTF16("test text"), AutocompleteMatch(),
145 AutocompleteMatch());
146 EXPECT_EQ(ChromeOmniboxNavigationObserver::LOAD_NOT_SEEN,
147 observer->load_state());
148
149 std::unique_ptr<content::NavigationEntry> entry =
150 content::NavigationController::CreateNavigationEntry(
151 GURL(), content::Referrer(), base::nullopt,
152 ui::PAGE_TRANSITION_FROM_ADDRESS_BAR, false, std::string(), profile(),
153 nullptr /* blob_url_loader_factory */);
154
155 content::NotificationService::current()->Notify(
156 content::NOTIFICATION_NAV_ENTRY_PENDING,
157 content::Source<content::NavigationController>(navigation_controller()),
158 content::Details<content::NavigationEntry>(entry.get()));
159
160 // A pending navigation notification should synchronously update the load
161 // state to pending.
162 EXPECT_EQ(ChromeOmniboxNavigationObserver::LOAD_PENDING,
163 observer->load_state());
164 }
165
TEST_F(ChromeOmniboxNavigationObserverTest,DeleteBrokenCustomSearchEngines)166 TEST_F(ChromeOmniboxNavigationObserverTest, DeleteBrokenCustomSearchEngines) {
167 struct TestData {
168 base::string16 keyword;
169 int status_code;
170 bool expect_exists;
171 };
172 std::vector<TestData> cases = {
173 {auto_generated_search_keyword(), 200, true},
174 {auto_generated_search_keyword(), 404, false},
175 {non_auto_generated_search_keyword(), 404, true},
176 {default_search_keyword(), 404, true},
177 {prepopulated_search_keyword(), 404, true},
178 {policy_search_keyword(), 404, true}};
179
180 base::string16 query = base::ASCIIToUTF16(" text");
181 for (size_t i = 0; i < cases.size(); ++i) {
182 SCOPED_TRACE("case #" + base::NumberToString(i));
183 // The keyword should always exist at the beginning.
184 EXPECT_TRUE(model()->GetTemplateURLForKeyword(cases[i].keyword));
185
186 AutocompleteMatch match;
187 match.keyword = cases[i].keyword;
188 // |observer| gets deleted by observer->NavigationEntryCommitted().
189 ChromeOmniboxNavigationObserver* observer =
190 new ChromeOmniboxNavigationObserver(profile(), cases[i].keyword + query,
191 match, AutocompleteMatch());
192 auto navigation_entry =
193 content::NavigationController::CreateNavigationEntry(
194 GURL(), content::Referrer(), base::nullopt,
195 ui::PAGE_TRANSITION_FROM_ADDRESS_BAR, false, std::string(),
196 profile(), nullptr /* blob_url_loader_factory */);
197 content::LoadCommittedDetails details;
198 details.http_status_code = cases[i].status_code;
199 details.entry = navigation_entry.get();
200 observer->NavigationEntryCommitted(details);
201 EXPECT_EQ(cases[i].expect_exists,
202 model()->GetTemplateURLForKeyword(cases[i].keyword) != nullptr);
203 }
204
205 // Also run a URL navigation that results in a 404 through the system to make
206 // sure nothing crashes for regular URL navigations.
207 // |observer| gets deleted by observer->NavigationEntryCommitted().
208 ChromeOmniboxNavigationObserver* observer =
209 new ChromeOmniboxNavigationObserver(
210 profile(), base::ASCIIToUTF16("url navigation"), AutocompleteMatch(),
211 AutocompleteMatch());
212 auto navigation_entry = content::NavigationController::CreateNavigationEntry(
213 GURL(), content::Referrer(), base::nullopt,
214 ui::PAGE_TRANSITION_FROM_ADDRESS_BAR, false, std::string(), profile(),
215 nullptr /* blob_url_loader_factory */);
216 content::LoadCommittedDetails details;
217 details.http_status_code = 404;
218 details.entry = navigation_entry.get();
219 observer->NavigationEntryCommitted(details);
220 }
221
TEST_F(ChromeOmniboxNavigationObserverTest,AlternateNavInfoBar)222 TEST_F(ChromeOmniboxNavigationObserverTest, AlternateNavInfoBar) {
223 TestURLLoaderFactory test_url_loader_factory;
224 scoped_refptr<network::SharedURLLoaderFactory> shared_factory =
225 base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
226 &test_url_loader_factory);
227
228 const int kNetError = 0;
229 const int kNoResponse = -1;
230 struct Response {
231 const std::vector<std::string> urls; // If more than one, 301 between them.
232 // The final status code to return after all the redirects, or one of
233 // kNetError or kNoResponse.
234 const int http_response_code;
235 std::string content;
236 };
237
238 // All of these test cases assume the alternate nav URL is http://example/.
239 struct Case {
240 const Response response;
241 const bool expected_alternate_nav_bar_shown;
242 } cases[] = {
243 // The only response provided is a net error.
244 {{{"http://example/"}, kNetError}, false},
245 // The response connected to a valid page.
246 {{{"http://example/"}, 200}, true},
247 // A non-empty page, despite the HEAD.
248 {{{"http://example/"}, 200, "Content"}, true},
249 // The response connected to an error page.
250 {{{"http://example/"}, 404}, false},
251
252 // The response redirected to same host, just http->https, with a path
253 // change as well. In this case the second URL should not fetched; Chrome
254 // will optimistically assume the destination will return a valid page and
255 // display the infobar.
256 {{{"http://example/", "https://example/path"}, kNoResponse}, true},
257 // Ditto, making sure it still holds when the final destination URL
258 // returns a valid status code.
259 {{{"http://example/", "https://example/path"}, 200}, true},
260
261 // The response redirected to an entirely different host. In these cases,
262 // no URL should be fetched against this second host; again Chrome will
263 // optimistically assume the destination will return a valid page and
264 // display the infobar.
265 {{{"http://example/", "http://new-destination/"}, kNoResponse}, true},
266 // Ditto, making sure it still holds when the final destination URL
267 // returns a valid status code.
268 {{{"http://example/", "http://new-destination/"}, 200}, true},
269
270 // The response redirected to same host, just http->https, with no other
271 // changes. In these cases, Chrome will fetch the second URL.
272 // The second URL response returned a valid page.
273 {{{"http://example/", "https://example/"}, 200}, true},
274 // The second URL response returned an error page.
275 {{{"http://example/", "https://example/"}, 404}, false},
276 // The second URL response returned a net error.
277 {{{"http://example/", "https://example/"}, kNetError}, false},
278 // The second URL response redirected again.
279 {{{"http://example/", "https://example/", "https://example/root"},
280 kNoResponse},
281 true},
282 };
283 for (size_t i = 0; i < base::size(cases); ++i) {
284 SCOPED_TRACE("case #" + base::NumberToString(i));
285 const Case& test_case = cases[i];
286 const Response& response = test_case.response;
287
288 // Set the URL request responses.
289 test_url_loader_factory.ClearResponses();
290
291 // Compute URL redirect chain.
292 TestURLLoaderFactory::Redirects redirects;
293 for (size_t dest = 1; dest < response.urls.size(); ++dest) {
294 net::RedirectInfo redir_info;
295 redir_info.new_url = GURL(response.urls[dest]);
296 redir_info.status_code = net::HTTP_MOVED_PERMANENTLY;
297 auto redir_head =
298 network::CreateURLResponseHead(net::HTTP_MOVED_PERMANENTLY);
299 redirects.push_back({redir_info, std::move(redir_head)});
300 }
301
302 // Fill in final response.
303 network::mojom::URLResponseHeadPtr http_head =
304 network::mojom::URLResponseHead::New();
305 network::URLLoaderCompletionStatus net_status;
306 network::TestURLLoaderFactory::ResponseProduceFlags response_flags =
307 network::TestURLLoaderFactory::kResponseDefault;
308
309 if (response.http_response_code == kNoResponse) {
310 response_flags =
311 TestURLLoaderFactory::kResponseOnlyRedirectsNoDestination;
312 } else if (response.http_response_code == kNetError) {
313 net_status = network::URLLoaderCompletionStatus(net::ERR_FAILED);
314 } else {
315 net_status = network::URLLoaderCompletionStatus(net::OK);
316 http_head = network::CreateURLResponseHead(
317 static_cast<net::HttpStatusCode>(response.http_response_code));
318 }
319
320 test_url_loader_factory.AddResponse(GURL(response.urls[0]),
321 std::move(http_head), response.content,
322 net_status, std::move(redirects));
323
324 // Create the alternate nav match and the observer.
325 // |observer| gets deleted automatically after all fetchers complete.
326 AutocompleteMatch alternate_nav_match;
327 alternate_nav_match.destination_url = GURL("http://example/");
328 bool displayed_infobar;
329 ChromeOmniboxNavigationObserver* observer =
330 new MockChromeOmniboxNavigationObserver(
331 profile(), base::ASCIIToUTF16("example"), AutocompleteMatch(),
332 alternate_nav_match, &displayed_infobar);
333 observer->SetURLLoaderFactoryForTesting(shared_factory);
334
335 // Send the observer NAV_ENTRY_PENDING to get the URL fetcher to start.
336 auto navigation_entry =
337 content::NavigationController::CreateNavigationEntry(
338 GURL(), content::Referrer(), base::nullopt,
339 ui::PAGE_TRANSITION_FROM_ADDRESS_BAR, false, std::string(),
340 profile(), nullptr /* blob_url_loader_factory */);
341 content::NotificationService::current()->Notify(
342 content::NOTIFICATION_NAV_ENTRY_PENDING,
343 content::Source<content::NavigationController>(navigation_controller()),
344 content::Details<content::NavigationEntry>(navigation_entry.get()));
345
346 // Make sure the fetcher(s) have finished.
347 base::RunLoop().RunUntilIdle();
348
349 // Send the observer NavigationEntryCommitted() to get it to display the
350 // infobar if needed.
351 content::LoadCommittedDetails details;
352 details.http_status_code = 200;
353 details.entry = navigation_entry.get();
354 observer->NavigationEntryCommitted(details);
355
356 // See if AlternateNavInfoBarDelegate::Create() was called.
357 EXPECT_EQ(test_case.expected_alternate_nav_bar_shown, displayed_infobar);
358 }
359 }
360