// Copyright 2013 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 #include "base/command_line.h" #include "base/metrics/histogram_samples.h" #include "base/metrics/statistics_recorder.h" #include "base/run_loop.h" #include "base/stl_util.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/infobars/infobar_service.h" #include "chrome/browser/password_manager/chrome_password_manager_client.h" #include "chrome/browser/password_manager/password_store_factory.h" #include "chrome/browser/password_manager/test_password_store_service.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/login/login_prompt.h" #include "chrome/browser/ui/login/login_prompt_test_utils.h" #include "chrome/browser/ui/passwords/manage_passwords_ui_controller.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/chrome_version_info.h" #include "chrome/test/base/in_process_browser_test.h" #include "chrome/test/base/test_switches.h" #include "chrome/test/base/ui_test_utils.h" #include "components/autofill/core/browser/autofill_test_utils.h" #include "components/infobars/core/confirm_infobar_delegate.h" #include "components/infobars/core/infobar.h" #include "components/infobars/core/infobar_manager.h" #include "components/password_manager/core/browser/test_password_store.h" #include "components/password_manager/core/common/password_manager_switches.h" #include "content/public/browser/navigation_controller.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_view_host.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_observer.h" #include "content/public/common/content_switches.h" #include "content/public/test/browser_test_utils.h" #include "content/public/test/test_utils.h" #include "net/test/embedded_test_server/embedded_test_server.h" #include "net/test/embedded_test_server/http_request.h" #include "net/test/embedded_test_server/http_response.h" #include "net/test/spawned_test_server/spawned_test_server.h" #include "net/url_request/test_url_fetcher_factory.h" #include "testing/gmock/include/gmock/gmock.h" #include "third_party/WebKit/public/web/WebInputEvent.h" #include "ui/events/keycodes/keyboard_codes.h" #include "ui/gfx/geometry/point.h" // NavigationObserver --------------------------------------------------------- namespace { // Observer that waits for navigation to complete and for the password infobar // to be shown. class NavigationObserver : public content::WebContentsObserver { public: explicit NavigationObserver(content::WebContents* web_contents) : content::WebContentsObserver(web_contents), message_loop_runner_(new content::MessageLoopRunner) {} virtual ~NavigationObserver() {} // Normally Wait() will not return until a main frame navigation occurs. // If a path is set, Wait() will return after this path has been seen, // regardless of the frame that navigated. Useful for multi-frame pages. void SetPathToWaitFor(const std::string& path) { wait_for_path_ = path; } // content::WebContentsObserver: virtual void DidFinishLoad(content::RenderFrameHost* render_frame_host, const GURL& validated_url) OVERRIDE { if (!wait_for_path_.empty()) { if (validated_url.path() == wait_for_path_) message_loop_runner_->Quit(); } else if (!render_frame_host->GetParent()) { message_loop_runner_->Quit(); } } void Wait() { message_loop_runner_->Run(); } private: std::string wait_for_path_; scoped_refptr message_loop_runner_; DISALLOW_COPY_AND_ASSIGN(NavigationObserver); }; // Observes the save password prompt (bubble or infobar) for a specified // WebContents, keeps track of whether or not it is currently shown, and allows // accepting saving passwords through it. class PromptObserver { public: virtual ~PromptObserver() {} // Checks if the prompt is being currently shown. virtual bool IsShowingPrompt() const = 0; // Expecting that the prompt is shown, saves the password. Checks that the // prompt is no longer visible afterwards. void Accept() const { EXPECT_TRUE(IsShowingPrompt()); AcceptImpl(); } // Chooses the right implementation of PromptObserver and creates an instance // of it. static scoped_ptr Create(content::WebContents* web_contents); protected: PromptObserver() {} // Accepts the password. The implementation can assume that the prompt is // currently shown, but is required to verify that the prompt is eventually // closed. virtual void AcceptImpl() const = 0; private: DISALLOW_COPY_AND_ASSIGN(PromptObserver); }; class InfoBarObserver : public PromptObserver, public infobars::InfoBarManager::Observer { public: explicit InfoBarObserver(content::WebContents* web_contents) : infobar_is_being_shown_(false), infobar_service_(InfoBarService::FromWebContents(web_contents)) { infobar_service_->AddObserver(this); } virtual ~InfoBarObserver() { if (infobar_service_) infobar_service_->RemoveObserver(this); } private: // PromptObserver: virtual bool IsShowingPrompt() const OVERRIDE { return infobar_is_being_shown_; } virtual void AcceptImpl() const OVERRIDE { EXPECT_EQ(1u, infobar_service_->infobar_count()); if (!infobar_service_->infobar_count()) return; // Let the test finish to gather possibly more diagnostics. // ConfirmInfoBarDelegate::Accept returning true means the infobar is // immediately closed. Checking the return value is preferred to testing // IsShowingPrompt() here, for it avoids the delay until the closing // notification is received. EXPECT_TRUE(infobar_service_->infobar_at(0) ->delegate() ->AsConfirmInfoBarDelegate() ->Accept()); } // infobars::InfoBarManager::Observer: virtual void OnInfoBarAdded(infobars::InfoBar* infobar) OVERRIDE { infobar_is_being_shown_ = true; } virtual void OnInfoBarRemoved(infobars::InfoBar* infobar, bool animate) OVERRIDE { infobar_is_being_shown_ = false; } virtual void OnManagerShuttingDown( infobars::InfoBarManager* manager) OVERRIDE { ASSERT_EQ(infobar_service_, manager); infobar_service_->RemoveObserver(this); infobar_service_ = NULL; } bool infobar_is_being_shown_; InfoBarService* infobar_service_; DISALLOW_COPY_AND_ASSIGN(InfoBarObserver); }; class BubbleObserver : public PromptObserver { public: explicit BubbleObserver(content::WebContents* web_contents) : ui_controller_( ManagePasswordsUIController::FromWebContents(web_contents)) {} virtual ~BubbleObserver() {} private: // PromptObserver: virtual bool IsShowingPrompt() const OVERRIDE { return ui_controller_->PasswordPendingUserDecision(); } virtual void AcceptImpl() const OVERRIDE { ui_controller_->SavePassword(); EXPECT_FALSE(IsShowingPrompt()); } ManagePasswordsUIController* const ui_controller_; DISALLOW_COPY_AND_ASSIGN(BubbleObserver); }; // static scoped_ptr PromptObserver::Create( content::WebContents* web_contents) { if (ChromePasswordManagerClient::IsTheHotNewBubbleUIEnabled()) { return scoped_ptr(new BubbleObserver(web_contents)); } else { return scoped_ptr(new InfoBarObserver(web_contents)); } } // Handles |request| to "/basic_auth". If "Authorization" header is present, // responds with a non-empty HTTP 200 page (regardless of its value). Otherwise // serves a Basic Auth challenge. scoped_ptr HandleTestAuthRequest( const net::test_server::HttpRequest& request) { if (!StartsWithASCII(request.relative_url, "/basic_auth", true)) return scoped_ptr(); if (ContainsKey(request.headers, "Authorization")) { scoped_ptr http_response( new net::test_server::BasicHttpResponse); http_response->set_code(net::HTTP_OK); http_response->set_content("Success!"); return http_response.PassAs(); } else { scoped_ptr http_response( new net::test_server::BasicHttpResponse); http_response->set_code(net::HTTP_UNAUTHORIZED); http_response->AddCustomHeader("WWW-Authenticate", "Basic realm=\"test realm\""); return http_response.PassAs(); } } } // namespace // PasswordManagerBrowserTest ------------------------------------------------- class PasswordManagerBrowserTest : public InProcessBrowserTest { public: PasswordManagerBrowserTest() {} virtual ~PasswordManagerBrowserTest() {} // InProcessBrowserTest: virtual void SetUpOnMainThread() OVERRIDE { // Use TestPasswordStore to remove a possible race. Normally the // PasswordStore does its database manipulation on the DB thread, which // creates a possible race during navigation. Specifically the // PasswordManager will ignore any forms in a page if the load from the // PasswordStore has not completed. PasswordStoreFactory::GetInstance()->SetTestingFactory( browser()->profile(), TestPasswordStoreService::Build); ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady()); ASSERT_FALSE(CommandLine::ForCurrentProcess()->HasSwitch( password_manager::switches::kEnableAutomaticPasswordSaving)); } virtual void TearDownOnMainThread() OVERRIDE { ASSERT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete()); } protected: content::WebContents* WebContents() { return browser()->tab_strip_model()->GetActiveWebContents(); } content::RenderViewHost* RenderViewHost() { return WebContents()->GetRenderViewHost(); } // Wrapper around ui_test_utils::NavigateToURL that waits until // DidFinishLoad() fires. Normally this function returns after // DidStopLoading(), which caused flakiness as the NavigationObserver // would sometimes see the DidFinishLoad event from a previous navigation and // return immediately. void NavigateToFile(const std::string& path) { NavigationObserver observer(WebContents()); GURL url = embedded_test_server()->GetURL(path); ui_test_utils::NavigateToURL(browser(), url); observer.Wait(); } // Waits until the "value" attribute of the HTML element with |element_id| is // equal to |expected_value|. If the current value is not as expected, this // waits until the "change" event is fired for the element. This also // guarantees that once the real value matches the expected, the JavaScript // event loop is spun to allow all other possible events to take place. void WaitForElementValue(const std::string& element_id, const std::string& expected_value); // Checks that the current "value" attribute of the HTML element with // |element_id| is equal to |expected_value|. void CheckElementValue(const std::string& element_id, const std::string& expected_value); private: DISALLOW_COPY_AND_ASSIGN(PasswordManagerBrowserTest); }; void PasswordManagerBrowserTest::WaitForElementValue( const std::string& element_id, const std::string& expected_value) { enum ReturnCodes { // Possible results of the JavaScript code. RETURN_CODE_OK, RETURN_CODE_NO_ELEMENT, RETURN_CODE_WRONG_VALUE, RETURN_CODE_INVALID, }; const std::string value_check_function = base::StringPrintf( "function valueCheck() {" " var element = document.getElementById('%s');" " return element && element.value == '%s';" "}", element_id.c_str(), expected_value.c_str()); const std::string script = value_check_function + base::StringPrintf( "if (valueCheck()) {" " /* Spin the event loop with setTimeout. */" " setTimeout(window.domAutomationController.send(%d), 0);" "} else {" " var element = document.getElementById('%s');" " if (!element)" " window.domAutomationController.send(%d);" " element.onchange = function() {" " if (valueCheck()) {" " /* Spin the event loop with setTimeout. */" " setTimeout(window.domAutomationController.send(%d), 0);" " } else {" " window.domAutomationController.send(%d);" " }" " };" "}", RETURN_CODE_OK, element_id.c_str(), RETURN_CODE_NO_ELEMENT, RETURN_CODE_OK, RETURN_CODE_WRONG_VALUE); int return_value = RETURN_CODE_INVALID; ASSERT_TRUE(content::ExecuteScriptAndExtractInt( RenderViewHost(), script, &return_value)); EXPECT_EQ(RETURN_CODE_OK, return_value) << "element_id = " << element_id << ", expected_value = " << expected_value; } void PasswordManagerBrowserTest::CheckElementValue( const std::string& element_id, const std::string& expected_value) { const std::string value_check_script = base::StringPrintf( "var element = document.getElementById('%s');" "window.domAutomationController.send(element && element.value == '%s');", element_id.c_str(), expected_value.c_str()); bool return_value = false; ASSERT_TRUE(content::ExecuteScriptAndExtractBool( RenderViewHost(), value_check_script, &return_value)); EXPECT_TRUE(return_value) << "element_id = " << element_id << ", expected_value = " << expected_value; } // Actual tests --------------------------------------------------------------- IN_PROC_BROWSER_TEST_F(PasswordManagerBrowserTest, PromptForNormalSubmit) { NavigateToFile("/password/password_form.html"); // Fill a form and submit through a button. Nothing // special. NavigationObserver observer(WebContents()); scoped_ptr prompt_observer( PromptObserver::Create(WebContents())); std::string fill_and_submit = "document.getElementById('username_field').value = 'temp';" "document.getElementById('password_field').value = 'random';" "document.getElementById('input_submit_button').click()"; ASSERT_TRUE(content::ExecuteScript(RenderViewHost(), fill_and_submit)); observer.Wait(); EXPECT_TRUE(prompt_observer->IsShowingPrompt()); } IN_PROC_BROWSER_TEST_F(PasswordManagerBrowserTest, PromptForSubmitWithInPageNavigation) { NavigateToFile("/password/password_navigate_before_submit.html"); // Fill a form and submit through a button. Nothing // special. The form does an in-page navigation before submitting. NavigationObserver observer(WebContents()); scoped_ptr prompt_observer( PromptObserver::Create(WebContents())); std::string fill_and_submit = "document.getElementById('username_field').value = 'temp';" "document.getElementById('password_field').value = 'random';" "document.getElementById('input_submit_button').click()"; ASSERT_TRUE(content::ExecuteScript(RenderViewHost(), fill_and_submit)); observer.Wait(); EXPECT_TRUE(prompt_observer->IsShowingPrompt()); } IN_PROC_BROWSER_TEST_F(PasswordManagerBrowserTest, LoginSuccessWithUnrelatedForm) { // Log in, see a form on the landing page. That form is not related to the // login form (=has a different action), so we should offer saving the // password. NavigateToFile("/password/password_form.html"); NavigationObserver observer(WebContents()); scoped_ptr prompt_observer( PromptObserver::Create(WebContents())); std::string fill_and_submit = "document.getElementById('username_unrelated').value = 'temp';" "document.getElementById('password_unrelated').value = 'random';" "document.getElementById('submit_unrelated').click()"; ASSERT_TRUE(content::ExecuteScript(RenderViewHost(), fill_and_submit)); observer.Wait(); EXPECT_TRUE(prompt_observer->IsShowingPrompt()); } IN_PROC_BROWSER_TEST_F(PasswordManagerBrowserTest, LoginFailed) { // Log in, see a form on the landing page. That form is not related to the // login form (=has a different action), so we should offer saving the // password. NavigateToFile("/password/password_form.html"); NavigationObserver observer(WebContents()); scoped_ptr prompt_observer( PromptObserver::Create(WebContents())); std::string fill_and_submit = "document.getElementById('username_failed').value = 'temp';" "document.getElementById('password_failed').value = 'random';" "document.getElementById('submit_failed').click()"; ASSERT_TRUE(content::ExecuteScript(RenderViewHost(), fill_and_submit)); observer.Wait(); EXPECT_FALSE(prompt_observer->IsShowingPrompt()); } IN_PROC_BROWSER_TEST_F(PasswordManagerBrowserTest, Redirects) { NavigateToFile("/password/password_form.html"); // Fill a form and submit through a button. The form // points to a redirection page. NavigationObserver observer(WebContents()); scoped_ptr prompt_observer( PromptObserver::Create(WebContents())); std::string fill_and_submit = "document.getElementById('username_redirect').value = 'temp';" "document.getElementById('password_redirect').value = 'random';" "document.getElementById('submit_redirect').click()"; ASSERT_TRUE(content::ExecuteScript(RenderViewHost(), fill_and_submit)); observer.Wait(); EXPECT_TRUE(prompt_observer->IsShowingPrompt()); // The redirection page now redirects via Javascript. We check that the // infobar stays. ASSERT_TRUE(content::ExecuteScript(RenderViewHost(), "window.location.href = 'done.html';")); observer.Wait(); EXPECT_TRUE(prompt_observer->IsShowingPrompt()); } IN_PROC_BROWSER_TEST_F(PasswordManagerBrowserTest, PromptForSubmitUsingJavaScript) { NavigateToFile("/password/password_form.html"); // Fill a form and submit using