// 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/path_service.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/profiles/profile.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_paths.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/autofill/core/browser/test_autofill_client.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/content/browser/content_password_manager_driver.h" #include "components/password_manager/content/browser/content_password_manager_driver_factory.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/base/filename_util.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), quit_on_entry_commited_(false), message_loop_runner_(new content::MessageLoopRunner) {} ~NavigationObserver() override {} // 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; } // Normally Wait() will not return until a main frame navigation occurs. // If quit_on_entry_commited is true Wait() will return on EntryCommited. void SetQuitOnEntryCommitted(bool quit_on_entry_commited) { quit_on_entry_commited_ = quit_on_entry_commited; } // content::WebContentsObserver: 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 NavigationEntryCommitted( const content::LoadCommittedDetails& load_details) override { if (quit_on_entry_commited_) message_loop_runner_->Quit(); } void Wait() { message_loop_runner_->Run(); } private: std::string wait_for_path_; bool quit_on_entry_commited_; 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); } ~InfoBarObserver() override { if (infobar_service_) infobar_service_->RemoveObserver(this); } private: // PromptObserver: bool IsShowingPrompt() const override { return infobar_is_being_shown_; } 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: void OnInfoBarAdded(infobars::InfoBar* infobar) override { infobar_is_being_shown_ = true; } void OnInfoBarRemoved(infobars::InfoBar* infobar, bool animate) override { infobar_is_being_shown_ = false; } void OnManagerShuttingDown(infobars::InfoBarManager* manager) override { ASSERT_EQ(infobar_service_, manager); infobar_service_->RemoveObserver(this); infobar_service_ = nullptr; } 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)) {} ~BubbleObserver() override {} private: // PromptObserver: bool IsShowingPrompt() const override { return ui_controller_->PasswordPendingUserDecision(); } void AcceptImpl() const override { ui_controller_->SavePassword(); EXPECT_FALSE(IsShowingPrompt()); } ManagePasswordsUIController* const ui_controller_; DISALLOW_COPY_AND_ASSIGN(BubbleObserver); }; class ObservingAutofillClient : public autofill::TestAutofillClient { public: ObservingAutofillClient() : message_loop_runner_(new content::MessageLoopRunner){} ~ObservingAutofillClient() override {} void Wait() { message_loop_runner_->Run(); } void ShowAutofillPopup( const gfx::RectF& element_bounds, base::i18n::TextDirection text_direction, const std::vector& suggestions, base::WeakPtr delegate) override { message_loop_runner_->Quit(); } private: scoped_refptr message_loop_runner_; DISALLOW_COPY_AND_ASSIGN(ObservingAutofillClient); }; GURL GetFileURL(const char* filename) { base::FilePath path; PathService::Get(chrome::DIR_TEST_DATA, &path); path = path.AppendASCII("password").AppendASCII(filename); CHECK(base::PathExists(path)); return net::FilePathToFileURL(path); } // 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.Pass(); } 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.Pass(); } } } // namespace // PasswordManagerBrowserTest ------------------------------------------------- class PasswordManagerBrowserTest : public InProcessBrowserTest { public: PasswordManagerBrowserTest() {} ~PasswordManagerBrowserTest() override {} // InProcessBrowserTest: 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(base::CommandLine::ForCurrentProcess()->HasSwitch( password_manager::switches::kEnableAutomaticPasswordSaving)); } 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); // TODO(dvadym): Remove this once history.pushState() handling is not behind // a flag. // Calling this will activate handling of pushState()-initiated form submits. // This feature is currently behind a renderer flag. Just setting the flag in // the browser will not change it for the already running renderer at tab 0. // Therefore this method first sets the flag and then opens a new tab at // position 0. This new tab gets the flag copied from the browser process. void ActivateHistoryPushState(); 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; } void PasswordManagerBrowserTest::ActivateHistoryPushState() { base::CommandLine::ForCurrentProcess()->AppendSwitch( autofill::switches::kEnablePasswordSaveOnInPageNavigation); AddTabAtIndex(0, GURL(url::kAboutBlankURL), ui::PAGE_TRANSITION_TYPED); } // 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