1 // Copyright (c) 2012 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.
7 #include "base/callback_list.h"
8 #include "base/file_util.h"
9 #include "base/files/file_path.h"
10 #include "base/strings/string_number_conversions.h"
11 #include "base/strings/stringprintf.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "chrome/browser/chrome_notification_types.h"
14 #include "chrome/browser/extensions/api/identity/identity_api.h"
15 #include "chrome/browser/extensions/api/management/management_api.h"
16 #include "chrome/browser/extensions/api/webstore_private/webstore_private_api.h"
17 #include "chrome/browser/extensions/extension_apitest.h"
18 #include "chrome/browser/extensions/extension_function_test_utils.h"
19 #include "chrome/browser/extensions/extension_install_prompt.h"
20 #include "chrome/browser/extensions/extension_install_ui.h"
21 #include "chrome/browser/extensions/extension_service.h"
22 #include "chrome/browser/extensions/webstore_installer.h"
23 #include "chrome/browser/profiles/profile.h"
24 #include "chrome/browser/signin/fake_profile_oauth2_token_service.h"
25 #include "chrome/browser/signin/fake_profile_oauth2_token_service_builder.h"
26 #include "chrome/browser/signin/fake_signin_manager.h"
27 #include "chrome/browser/signin/profile_oauth2_token_service_factory.h"
28 #include "chrome/browser/signin/signin_manager_factory.h"
29 #include "chrome/browser/ui/browser.h"
30 #include "chrome/browser/ui/tabs/tab_strip_model.h"
31 #include "chrome/common/chrome_switches.h"
32 #include "chrome/test/base/ui_test_utils.h"
33 #include "components/keyed_service/content/browser_context_dependency_manager.h"
34 #include "components/signin/core/browser/signin_manager.h"
35 #include "components/signin/core/browser/test_signin_client.h"
36 #include "content/public/browser/gpu_data_manager.h"
37 #include "content/public/browser/notification_observer.h"
38 #include "content/public/browser/notification_registrar.h"
39 #include "content/public/test/browser_test_utils.h"
40 #include "gpu/config/gpu_feature_type.h"
41 #include "gpu/config/gpu_info.h"
42 #include "net/dns/mock_host_resolver.h"
43 #include "ui/gl/gl_switches.h"
45 using gpu::GpuFeatureType;
47 namespace utils = extension_function_test_utils;
49 namespace extensions {
53 class WebstoreInstallListener : public WebstoreInstaller::Delegate {
55 WebstoreInstallListener()
56 : received_failure_(false), received_success_(false), waiting_(false) {}
58 virtual void OnExtensionInstallSuccess(const std::string& id) OVERRIDE {
59 received_success_ = true;
64 base::MessageLoopForUI::current()->Quit();
68 virtual void OnExtensionInstallFailure(
69 const std::string& id,
70 const std::string& error,
71 WebstoreInstaller::FailureReason reason) OVERRIDE {
72 received_failure_ = true;
78 base::MessageLoopForUI::current()->Quit();
83 if (received_success_ || received_failure_)
87 content::RunMessageLoop();
89 bool received_success() const { return received_success_; }
90 const std::string& id() const { return id_; }
93 bool received_failure_;
94 bool received_success_;
102 // A base class for tests below.
103 class ExtensionWebstorePrivateApiTest : public ExtensionApiTest {
105 ExtensionWebstorePrivateApiTest()
106 : signin_manager_(NULL),
107 token_service_(NULL) {}
108 virtual ~ExtensionWebstorePrivateApiTest() {}
110 virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE {
111 ExtensionApiTest::SetUpCommandLine(command_line);
112 command_line->AppendSwitchASCII(
113 switches::kAppsGalleryURL,
114 "http://www.example.com/files/extensions/api_test");
115 command_line->AppendSwitchASCII(
116 switches::kAppsGalleryInstallAutoConfirmForTests, "accept");
119 virtual void SetUpInProcessBrowserTestFixture() OVERRIDE {
120 ExtensionApiTest::SetUpInProcessBrowserTestFixture();
122 // Start up the test server and get us ready for calling the install
124 host_resolver()->AddRule("www.example.com", "127.0.0.1");
125 ASSERT_TRUE(StartSpawnedTestServer());
126 ExtensionInstallUI::set_disable_failure_ui_for_tests();
128 will_create_browser_context_services_subscription_ =
129 BrowserContextDependencyManager::GetInstance()->
130 RegisterWillCreateBrowserContextServicesCallbackForTesting(
132 &ExtensionWebstorePrivateApiTest::
133 OnWillCreateBrowserContextServices,
134 base::Unretained(this))).Pass();
137 void OnWillCreateBrowserContextServices(content::BrowserContext* context) {
138 // Replace the signin manager and token service with fakes. Do this ahead of
139 // creating the browser so that a bunch of classes don't register as
140 // observers and end up needing to unregister when the fake is substituted.
141 SigninManagerFactory::GetInstance()->SetTestingFactory(
142 context, &FakeSigninManagerBase::Build);
143 ProfileOAuth2TokenServiceFactory::GetInstance()->SetTestingFactory(
144 context, &BuildFakeProfileOAuth2TokenService);
147 virtual void SetUpOnMainThread() OVERRIDE {
148 ExtensionApiTest::SetUpOnMainThread();
150 // Grab references to the fake signin manager and token service.
152 static_cast<FakeSigninManagerForTesting*>(
153 SigninManagerFactory::GetInstance()->GetForProfile(profile()));
154 ASSERT_TRUE(signin_manager_);
156 static_cast<FakeProfileOAuth2TokenService*>(
157 ProfileOAuth2TokenServiceFactory::GetInstance()->GetForProfile(
159 ASSERT_TRUE(token_service_);
161 ASSERT_TRUE(webstore_install_dir_.CreateUniqueTempDir());
162 webstore_install_dir_copy_ = webstore_install_dir_.path();
163 WebstoreInstaller::SetDownloadDirectoryForTests(
164 &webstore_install_dir_copy_);
168 // Returns a test server URL, but with host 'www.example.com' so it matches
169 // the web store app's extent that we set up via command line flags.
170 virtual GURL GetTestServerURL(const std::string& path) {
171 GURL url = test_server()->GetURL(
172 std::string("files/extensions/api_test/webstore_private/") + path);
174 // Replace the host with 'www.example.com' so it matches the web store
176 GURL::Replacements replace_host;
177 std::string host_str("www.example.com");
178 replace_host.SetHostStr(host_str);
180 return url.ReplaceComponents(replace_host);
183 // Navigates to |page| and runs the Extension API test there. Any downloads
184 // of extensions will return the contents of |crx_file|.
185 bool RunInstallTest(const std::string& page, const std::string& crx_file) {
186 // Auto-confirm the uninstallation dialog.
187 ManagementUninstallFunction::SetAutoConfirmForTest(true);
188 #if defined(OS_WIN) && !defined(NDEBUG)
189 // See http://crbug.com/177163 for details.
192 GURL crx_url = GetTestServerURL(crx_file);
193 CommandLine::ForCurrentProcess()->AppendSwitchASCII(
194 switches::kAppsGalleryUpdateURL, crx_url.spec());
196 GURL page_url = GetTestServerURL(page);
197 return RunPageTest(page_url.spec());
201 // Navigates to |page| and waits for the API call.
202 void StartSignInTest(const std::string& page) {
203 ui_test_utils::NavigateToURL(browser(), GetTestServerURL(page));
205 // Wait for the API to be called. A simple way to wait for this is to run
206 // some other JavaScript in the page and wait for a round-trip back to the
210 content::ExecuteScriptAndExtractBool(
211 GetWebContents(), "window.domAutomationController.send(true)",
216 content::WebContents* GetWebContents() {
217 return browser()->tab_strip_model()->GetActiveWebContents();
220 ExtensionService* service() {
221 return browser()->profile()->GetExtensionService();
224 FakeSigninManagerForTesting* signin_manager_;
225 FakeProfileOAuth2TokenService* token_service_;
228 scoped_ptr<base::CallbackList<void(content::BrowserContext*)>::Subscription>
229 will_create_browser_context_services_subscription_;
231 base::ScopedTempDir webstore_install_dir_;
232 // WebstoreInstaller needs a reference to a FilePath when setting the download
233 // directory for testing.
234 base::FilePath webstore_install_dir_copy_;
237 // Test cases for webstore origin frame blocking.
238 // TODO(mkwst): Disabled until new X-Frame-Options behavior rolls into
239 // Chromium, see crbug.com/226018.
240 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest,
241 DISABLED_FrameWebstorePageBlocked) {
242 base::string16 expected_title = base::UTF8ToUTF16("PASS: about:blank");
243 base::string16 failure_title = base::UTF8ToUTF16("FAIL");
244 content::TitleWatcher watcher(GetWebContents(), expected_title);
245 watcher.AlsoWaitForTitle(failure_title);
246 GURL url = test_server()->GetURL(
247 "files/extensions/api_test/webstore_private/noframe.html");
248 ui_test_utils::NavigateToURL(browser(), url);
249 base::string16 final_title = watcher.WaitAndGetTitle();
250 EXPECT_EQ(expected_title, final_title);
253 // TODO(mkwst): Disabled until new X-Frame-Options behavior rolls into
254 // Chromium, see crbug.com/226018.
255 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest,
256 DISABLED_FrameErrorPageBlocked) {
257 base::string16 expected_title = base::UTF8ToUTF16("PASS: about:blank");
258 base::string16 failure_title = base::UTF8ToUTF16("FAIL");
259 content::TitleWatcher watcher(GetWebContents(), expected_title);
260 watcher.AlsoWaitForTitle(failure_title);
261 GURL url = test_server()->GetURL(
262 "files/extensions/api_test/webstore_private/noframe2.html");
263 ui_test_utils::NavigateToURL(browser(), url);
264 base::string16 final_title = watcher.WaitAndGetTitle();
265 EXPECT_EQ(expected_title, final_title);
268 // Test cases where the user accepts the install confirmation dialog.
269 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest, InstallAccepted) {
270 ASSERT_TRUE(RunInstallTest("accepted.html", "extension.crx"));
273 // Test having the default download directory missing.
274 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest, MissingDownloadDir) {
275 // Set a non-existent directory as the download path.
276 base::ScopedTempDir temp_dir;
277 EXPECT_TRUE(temp_dir.CreateUniqueTempDir());
278 base::FilePath missing_directory = temp_dir.Take();
279 EXPECT_TRUE(base::DeleteFile(missing_directory, true));
280 WebstoreInstaller::SetDownloadDirectoryForTests(&missing_directory);
282 // Now run the install test, which should succeed.
283 ASSERT_TRUE(RunInstallTest("accepted.html", "extension.crx"));
286 if (base::DirectoryExists(missing_directory))
287 EXPECT_TRUE(base::DeleteFile(missing_directory, true));
290 // Tests passing a localized name.
291 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest, InstallLocalized) {
292 ASSERT_TRUE(RunInstallTest("localized.html", "localized_extension.crx"));
295 // Now test the case where the user cancels the confirmation dialog.
296 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest, InstallCancelled) {
297 CommandLine::ForCurrentProcess()->AppendSwitchASCII(
298 switches::kAppsGalleryInstallAutoConfirmForTests, "cancel");
299 ASSERT_TRUE(RunInstallTest("cancelled.html", "extension.crx"));
302 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest, IncorrectManifest1) {
303 ASSERT_TRUE(RunInstallTest("incorrect_manifest1.html", "extension.crx"));
306 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest, IncorrectManifest2) {
307 ASSERT_TRUE(RunInstallTest("incorrect_manifest2.html", "extension.crx"));
310 // Disabled: http://crbug.com/174399 and http://crbug.com/177163
311 #if defined(OS_WIN) && (defined(USE_AURA) || !defined(NDEBUG))
312 #define MAYBE_AppInstallBubble DISABLED_AppInstallBubble
314 #define MAYBE_AppInstallBubble AppInstallBubble
317 // Tests that we can request an app installed bubble (instead of the default
318 // UI when an app is installed).
319 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest,
320 MAYBE_AppInstallBubble) {
321 WebstoreInstallListener listener;
322 WebstorePrivateApi::SetWebstoreInstallerDelegateForTesting(&listener);
323 ASSERT_TRUE(RunInstallTest("app_install_bubble.html", "app.crx"));
325 ASSERT_TRUE(listener.received_success());
326 ASSERT_EQ("iladmdjkfniedhfhcfoefgojhgaiaccc", listener.id());
329 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest, IsInIncognitoMode) {
330 GURL page_url = GetTestServerURL("incognito.html");
332 RunPageTest(page_url.spec(), ExtensionApiTest::kFlagUseIncognito));
335 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest, IsNotInIncognitoMode) {
336 GURL page_url = GetTestServerURL("not_incognito.html");
337 ASSERT_TRUE(RunPageTest(page_url.spec()));
340 // Fails often on Windows dbg bots. http://crbug.com/177163.
342 #define MAYBE_IconUrl DISABLED_IconUrl
344 #define MAYBE_IconUrl IconUrl
345 #endif // defined(OS_WIN)
346 // Tests using the iconUrl parameter to the install function.
347 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest, MAYBE_IconUrl) {
348 ASSERT_TRUE(RunInstallTest("icon_url.html", "extension.crx"));
351 // http://crbug.com/177163
352 #if defined(OS_WIN) && !defined(NDEBUG)
353 #define MAYBE_BeginInstall DISABLED_BeginInstall
355 #define MAYBE_BeginInstall BeginInstall
357 // Tests that the Approvals are properly created in beginInstall.
358 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest, MAYBE_BeginInstall) {
359 std::string appId = "iladmdjkfniedhfhcfoefgojhgaiaccc";
360 std::string extensionId = "enfkhcelefdadlmkffamgdlgplcionje";
361 ASSERT_TRUE(RunInstallTest("begin_install.html", "extension.crx"));
363 scoped_ptr<WebstoreInstaller::Approval> approval =
364 WebstorePrivateApi::PopApprovalForTesting(browser()->profile(), appId);
365 EXPECT_EQ(appId, approval->extension_id);
366 EXPECT_TRUE(approval->use_app_installed_bubble);
367 EXPECT_FALSE(approval->skip_post_install_ui);
368 EXPECT_EQ("2", approval->authuser);
369 EXPECT_EQ(browser()->profile(), approval->profile);
371 approval = WebstorePrivateApi::PopApprovalForTesting(
372 browser()->profile(), extensionId);
373 EXPECT_EQ(extensionId, approval->extension_id);
374 EXPECT_FALSE(approval->use_app_installed_bubble);
375 EXPECT_FALSE(approval->skip_post_install_ui);
376 EXPECT_TRUE(approval->authuser.empty());
377 EXPECT_EQ(browser()->profile(), approval->profile);
380 // http://crbug.com/177163
381 #if defined(OS_WIN) && !defined(NDEBUG)
382 #define MAYBE_InstallTheme DISABLED_InstallTheme
384 #define MAYBE_InstallTheme InstallTheme
386 // Tests that themes are installed without an install prompt.
387 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest, MAYBE_InstallTheme) {
388 WebstoreInstallListener listener;
389 WebstorePrivateApi::SetWebstoreInstallerDelegateForTesting(&listener);
390 ASSERT_TRUE(RunInstallTest("theme.html", "../../theme.crx"));
392 ASSERT_TRUE(listener.received_success());
393 ASSERT_EQ("iamefpfkojoapidjnbafmgkgncegbkad", listener.id());
396 // Tests that an error is properly reported when an empty crx is returned.
397 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest, EmptyCrx) {
398 ASSERT_TRUE(RunInstallTest("empty.html", "empty.crx"));
401 class ExtensionWebstoreGetWebGLStatusTest : public InProcessBrowserTest {
403 void RunTest(bool webgl_allowed) {
404 // If Gpu access is disallowed then WebGL will not be available.
405 if (!content::GpuDataManager::GetInstance()->GpuAccessAllowed(NULL))
406 webgl_allowed = false;
408 static const char kEmptyArgs[] = "[]";
409 static const char kWebGLStatusAllowed[] = "webgl_allowed";
410 static const char kWebGLStatusBlocked[] = "webgl_blocked";
411 scoped_refptr<WebstorePrivateGetWebGLStatusFunction> function =
412 new WebstorePrivateGetWebGLStatusFunction();
413 scoped_ptr<base::Value> result(utils::RunFunctionAndReturnSingleResult(
414 function.get(), kEmptyArgs, browser()));
416 EXPECT_EQ(base::Value::TYPE_STRING, result->GetType());
417 std::string webgl_status;
418 EXPECT_TRUE(result->GetAsString(&webgl_status));
419 EXPECT_STREQ(webgl_allowed ? kWebGLStatusAllowed : kWebGLStatusBlocked,
420 webgl_status.c_str());
424 // Tests getWebGLStatus function when WebGL is allowed.
425 IN_PROC_BROWSER_TEST_F(ExtensionWebstoreGetWebGLStatusTest, Allowed) {
426 bool webgl_allowed = true;
427 RunTest(webgl_allowed);
430 // Tests getWebGLStatus function when WebGL is blacklisted.
431 IN_PROC_BROWSER_TEST_F(ExtensionWebstoreGetWebGLStatusTest, Blocked) {
432 static const std::string json_blacklist =
434 " \"name\": \"gpu blacklist\",\n"
435 " \"version\": \"1.0\",\n"
445 gpu::GPUInfo gpu_info;
446 content::GpuDataManager::GetInstance()->InitializeForTesting(
447 json_blacklist, gpu_info);
448 EXPECT_TRUE(content::GpuDataManager::GetInstance()->IsFeatureBlacklisted(
449 gpu::GPU_FEATURE_TYPE_WEBGL));
451 bool webgl_allowed = false;
452 RunTest(webgl_allowed);
455 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest,
456 SignIn_UserGestureRequired) {
457 GURL page_url = GetTestServerURL("sign_in_user_gesture_required.html");
458 ASSERT_TRUE(RunPageTest(page_url.spec()));
461 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest,
462 SignIn_MissingContinueUrl) {
463 GURL page_url = GetTestServerURL("sign_in_missing_continue_url.html");
464 ASSERT_TRUE(RunPageTest(page_url.spec()));
467 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest,
468 SignIn_InvalidContinueUrl) {
469 GURL page_url = GetTestServerURL("sign_in_invalid_continue_url.html");
470 ASSERT_TRUE(RunPageTest(page_url.spec()));
473 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest,
474 SignIn_ContinueUrlOnDifferentOrigin) {
476 GetTestServerURL("sign_in_continue_url_on_different_origin.html");
477 ASSERT_TRUE(RunPageTest(page_url.spec()));
480 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest,
481 SignIn_DisallowedInIncognito) {
482 // Make sure that the test is testing something more than the absence of a
483 // sign-in manager for this profile.
484 ASSERT_TRUE(SigninManagerFactory::GetForProfile(profile()));
487 GetTestServerURL("sign_in_disallowed_in_incognito.html");
489 RunPageTest(page_url.spec(), ExtensionApiTest::kFlagUseIncognito));
492 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest,
493 SignIn_DisabledWhenWebBasedSigninIsEnabled) {
494 // Make sure that the test is testing something more than the absence of a
495 // sign-in manager for this profile.
496 ASSERT_TRUE(SigninManagerFactory::GetForProfile(profile()));
498 CommandLine::ForCurrentProcess()->AppendSwitch(
499 switches::kEnableWebBasedSignin);
500 GURL page_url = GetTestServerURL(
501 "sign_in_disabled_when_web_based_signin_is_enabled.html");
502 ASSERT_TRUE(RunPageTest(page_url.spec()));
505 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest,
506 SignIn_AlreadySignedIn) {
507 signin_manager_->SetAuthenticatedUsername("user@example.com");
508 GURL page_url = GetTestServerURL("sign_in_already_signed_in.html");
509 ASSERT_TRUE(RunPageTest(page_url.spec()));
512 // The FakeSignInManager class is not implemented for ChromeOS, so there's no
513 // straightforward way to test these flows on that platform.
514 #if !defined(OS_CHROMEOS)
515 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest,
516 SignIn_AuthInProgress_Fails) {
517 // Initiate an authentication that will be in progress when the sign-in API is
519 signin_manager_->set_auth_in_progress("user@example.com");
521 // Navigate to the page, which will cause the sign-in API to be called.
522 // Then, complete the authentication in a failed state.
523 ResultCatcher catcher;
524 StartSignInTest("sign_in_auth_in_progress_fails.html");
525 signin_manager_->FailSignin(GoogleServiceAuthError::AuthErrorNone());
526 ASSERT_TRUE(catcher.GetNextResult());
529 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest,
530 SignIn_AuthInProgress_MergeSessionFails) {
531 // Initiate an authentication that will be in progress when the sign-in API is
533 signin_manager_->set_auth_in_progress("user@example.com");
535 // Navigate to the page, which will cause the sign-in API to be called.
536 // Then, complete the authentication in a successful state.
537 ResultCatcher catcher;
538 StartSignInTest("sign_in_auth_in_progress_merge_session_fails.html");
539 signin_manager_->CompletePendingSignin();
540 token_service_->IssueRefreshTokenForUser("user@example.com", "token");
541 signin_manager_->NotifyMergeSessionObservers(
542 GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE));
543 ASSERT_TRUE(catcher.GetNextResult());
546 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest,
547 SignIn_AuthInProgress_Succeeds) {
548 // Initiate an authentication that will be in progress when the sign-in API is
550 signin_manager_->set_auth_in_progress("user@example.com");
552 // Navigate to the page, which will cause the sign-in API to be called.
553 // Then, complete the authentication in a successful state.
554 ResultCatcher catcher;
555 StartSignInTest("sign_in_auth_in_progress_succeeds.html");
556 signin_manager_->CompletePendingSignin();
557 token_service_->IssueRefreshTokenForUser("user@example.com", "token");
558 signin_manager_->NotifyMergeSessionObservers(
559 GoogleServiceAuthError::AuthErrorNone());
560 ASSERT_TRUE(catcher.GetNextResult());
562 #endif // !defined (OS_CHROMEOS)
564 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest,
565 SignIn_RedirectToSignIn) {
567 "chrome://chrome-signin/?source=5&"
568 "continue=http%3A%2F%2Fwww.example.com%3A" +
569 base::IntToString(test_server()->host_port_pair().port()) +
571 ui_test_utils::UrlLoadObserver observer(
573 content::Source<content::NavigationController>(
574 &GetWebContents()->GetController()));
575 StartSignInTest("sign_in_redirect_to_sign_in.html");
578 // TODO(isherman): Also test the redirect back to the continue URL once
579 // sign-in completes?
582 } // namespace extensions