diff options
author | rchtara@chromium.org <rchtara@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-05-23 16:07:43 +0000 |
---|---|---|
committer | rchtara@chromium.org <rchtara@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-05-23 16:07:43 +0000 |
commit | 2cfc162ce41dc4c92676634946b51ce04e4d8085 (patch) | |
tree | d27edda7fb1d184d6ac2dec4828967a926a906ae | |
parent | 057aac0ac4eaf7352846b3d2f88b4e913d8901c8 (diff) | |
download | chromium_src-2cfc162ce41dc4c92676634946b51ce04e4d8085.zip chromium_src-2cfc162ce41dc4c92676634946b51ce04e4d8085.tar.gz chromium_src-2cfc162ce41dc4c92676634946b51ce04e4d8085.tar.bz2 |
Password Manager testing automation
Adding automatic tests to the password manager to simulate user interaction with websites.
BUG=369521
Review URL: https://codereview.chromium.org/273523004
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@272545 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | components/test/data/password_manager/README | 177 | ||||
-rw-r--r-- | components/test/data/password_manager/environment.py | 328 | ||||
-rw-r--r-- | components/test/data/password_manager/tests.py | 515 | ||||
-rw-r--r-- | components/test/data/password_manager/websitetest.py | 406 |
4 files changed, 1426 insertions, 0 deletions
diff --git a/components/test/data/password_manager/README b/components/test/data/password_manager/README new file mode 100644 index 0000000..3bc561c --- /dev/null +++ b/components/test/data/password_manager/README @@ -0,0 +1,177 @@ +This file contains high-level info about how to use password manager tests and +how to create new ones. + +The password manager tests purpose is to allow automatic password manager +checking and avoiding to do so manually. +The tests are written in python using selenium Webdriver library. + + +=====Getting started===== + +Build ChromeDriver by building the 'chromedriver' target. This will +create an executable binary in the build folder named 'chromedriver[.exe]'. + +Build chrome too by building the 'chrome' target. This will +create an executable binary in the build folder named 'chrome[.exe]'. + +Install Selenium (the version tested was 2.41.0): +pip install -U selenium + + +For security reasons, we didn't publish the passwords and the usernames we +used to test. So we put them to an xml file (websites.xml). The structure of +the file is the following: +<websites> + <website name = "website name"> + <username>username</username> + <password>password</password> + </website> +<websites> +You can ask someone to give you the websites.xml file and put it in the same +folder as the tests. You can also create your own websites.xml with your +personal accounts. +WARNING: All the content of the PROFILEPATH is going to be deleted. +Show the help: +python tests.py --help +Run all the working tests tests by executing: +python tests.py --chrome-path CHROMEPATH --chromedriver-path CHROMEDRIVERPATH +--profile-path PROFILEPATH [--passwords_path PASSWORDSPATH] + +Run all the tests by executing: +python tests.py --all --chrome-path CHROMEPATH --chromedriver-path +CHROMEDRIVERPATH --profile-path PROFILEPATH [--passwords_path PASSWORDSPATH] + +Run one or many tests by executing: +python tests.py google --chrome-path CHROMEPATH --chromedriver-path +CHROMEDRIVERPATH profile-path PROFILEPATH [--passwords_path PASSWORDSPATH] + +python tests.py google facebook --chrome-path CHROMEPATH --chromedriver-path +CHROMEDRIVERPATH --profile-path PROFILEPATH [--passwords_path PASSWORDSPATH] + +python tests.py google facebook amazon --chrome-path CHROMEPATH +--chromedriver-path CHROMEDRIVERPATH --profile-path PROFILEPATH +[--passwords_path PASSWORDSPATH] + +To display the debugging messages on the screen, use: +python tests.py --log DEBUG|INFO|WARNING|ERROR|CRITICAL --log-screen +To save debugging messages into a file, use: +python tests.py --log DEBUG|INFO|WARNING|ERROR|CRITICAL --log-file LOG_FILE + + +=====Creating new test===== + +1) Open tests.py. + +2) Add tests like this: + +class NewWebsiteTest(WebsiteTest): + + def Login(self): + # Add login steps for the website, for example: + self.GoTo("http://url") + self.FillUsernameInto("Username CSS selector") + self.FillPasswordInto("Password CSS selector") + self.Submit("Password CSS selector") + + def Logout(self): + # Add logout steps for the website, for example: + self.Click("Logout button CSS selector") + +Then, to create the new test, you need just to add: + +environment.AddWebsiteTest(NewWebsiteTest("website name")) + +* For security reasons, passwords and usernames need to be supplied in a +separate XML file and never checked in to the repository. The XML file should +contain data structured like this: + +<website name = "website name"> + <username>username</username> + <password>password</password> +</website> + +The "website name" is only used to find the username and password in the xml +file. + + +Use the flowing methods to perform the login and logout: +The methods that you can use are: + +* Click: find an element using CSS Selector and click on it. Throw an +exception if the element is not visible. +self.Click("css_selector") +* ClickIfClickable: find an element using CSS Selector and click on it if it's +possible to do that. +self.ClickIfClickable("css_selector") +* GoTo: navigate the main frame to a url. +self.GoTo("url") +* HoverOver: find an element using CSS Selector and hover over it. +self.HoverOver("css_selector") +* SendEnterTo: find an element using CSS Selector and send enter to it. +self.SendEnterTo("css_selector") + +* IsDisplayed: check if an element is displayed. +self.IsDisplayed("css_selector") +* Wait: wait for some amount of time in seconds. +self.Wait(10) +* WaitUntilDisplayed: wait for an element defined using CSS Selector to be +displayed. +self.WaitUntilDisplayed("css_selector") + +* FillPasswordInto: find an input element using CSS Selector and fill it with +the password. +self.FillPasswordInto("css_selector") +* FillUsernameInto: find an input element using CSS Selector and fill it with +the username. +self.FillUsernameInto("css_selector") +* Submit: find an element using CSS Selector and call its submit() handler. +self.Submit("css_selector") + + +=====Files structure===== + +Classes: +* environment.py: the definition the tests Environment. +* websitetest.py: WebsiteTest is defined here. You need to create an instance +of this class for each website you want to test. + +Tests: +* tests.py: the tests setup and the configuration for each website happens +here. This file contain 3 separate kinds of tests: + +1) working tests: tests that are supposed to work. If you have a problem with +one of them, rerun it again. Or try using the Known Issues section to fix it. +2) tests that can cause a crash (the cause of the crash is not related to the +password manager): This means that this set is expected to become a working +test or failing test when the issue that causes the crash now is solved. +3) failing tests: tests that fail for known bug related to the password +manager. When this bug is solved, all the tests that were failing because of +it are going to be moved to working tests. + +Other files: +* websites.xml : a private file where you can find all the passwords. You can +ask someone to give it to you or just create your own with your personal +accounts. +<websites> + <website name = "website name"> + <username>username</username> + <password>password</password> + </website> +</websites> + + +=====Known Issues===== + +The tests are very fragile. Here are some suggestions for solving most of the +problems: +* Restart the tests. +* Remove the profile if the tests fail at the beginning for unknown reason. +* If tests fail, isolate the one that causes problem, read debugging messages +and keep your eyes on the browser window to understand its causes: +a) In the tests, we often need to wait for a menu to appear ... If the +menu takes more time to appear than expected, the tests are going to fail. +b) The websites change very often. And even if they are not changed, they some +time show a popup that broke the tests. In the case you need to login manually +to the website, close all popup and logout. +* If you are logged in when the tests crashes, don't forget to log out before +running the tests a second time. diff --git a/components/test/data/password_manager/environment.py b/components/test/data/password_manager/environment.py new file mode 100644 index 0000000..c0303de --- /dev/null +++ b/components/test/data/password_manager/environment.py @@ -0,0 +1,328 @@ +# Copyright 2014 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. + +"""The testing Environment class.""" + +import logging +import shutil +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.chrome.options import Options +from xml.etree import ElementTree + +# Message strings to look for in chrome://password-manager-internals +MESSAGE_ASK = "Message: Decision: ASK the user" +MESSAGE_SAVE = "Message: Decision: SAVE the password" + + +class Environment: + """Sets up the testing Environment. """ + + def __init__(self, chrome_path, chromedriver_path, profile_path, + passwords_path, enable_automatic_password_saving, + numeric_level=None, log_to_console=False, log_file=""): + """Creates a new testing Environment. + + Args: + chrome_path: The chrome binary file. + chromedriver_path: The chromedriver binary file. + profile_path: The chrome testing profile folder. + passwords_path: The usernames and passwords file. + enable_automatic_password_saving: If True, the passwords are going to be + saved without showing the prompt. + numeric_level: The log verbosity. + log_to_console: If True, the debug logs will be shown on the console. + log_file: The file where to store the log. If it's empty, the log will + not be stored. + + Raises: + Exception: An exception is raised if |profile_path| folder could not be + removed. + """ + # Setting up the login. + if numeric_level is not None: + if log_file: + # Set up logging to file. + logging.basicConfig(level=numeric_level, + filename=log_file, + filemode='w') + + if log_to_console: + console = logging.StreamHandler() + console.setLevel(numeric_level) + # Add the handler to the root logger. + logging.getLogger('').addHandler(console) + + elif log_to_console: + logging.basicConfig(level=numeric_level) + + # Cleaning the chrome testing profile folder. + try: + shutil.rmtree(profile_path) + except Exception, e: + # The tests execution can continue, but this make them less stable. + logging.error("Error: Could not wipe the chrome profile directory (%s). \ + This affects the stability of the tests. Continuing to run tests." + % e) + options = Options() + if enable_automatic_password_saving: + options.add_argument("enable-automatic-password-saving") + # Chrome path. + options.binary_location = chrome_path + # Chrome testing profile path. + options.add_argument("user-data-dir=%s" % profile_path) + + # The webdriver. It's possible to choose the port the service is going to + # run on. If it's left to 0, a free port will be found. + self.driver = webdriver.Chrome(chromedriver_path, 0, options) + # The password internals window. + self.internals_window = self.driver.current_window_handle + # Password internals page. + self.internals_page = "chrome://password-manager-internals/" + # The Website window. + self.website_window = None + # The WebsiteTests list. + self.websitetests = [] + # An xml tree filled with logins and passwords. + self.passwords_tree = ElementTree.parse(passwords_path).getroot() + # The enabled WebsiteTests list. + self.working_tests = [] + # Map messages to the number of their appearance in the log. + self.message_count = dict() + self.message_count[MESSAGE_ASK] = 0 + self.message_count[MESSAGE_SAVE] = 0 + # The tests needs two tabs to work. A new tab is opened with the first + # GoTo. This is why we store here whether or not it's the first time to + # execute GoTo. + self.first_go_to = True + + def AddWebsiteTest(self, websitetest, disabled=False): + """Adds a WebsiteTest to the testing Environment. + + Args: + websitetest: The WebsiteTest instance to be added. + disabled: Whether test is disabled. + """ + websitetest.environment = self + websitetest.driver = self.driver + if self.passwords_tree is not None: + if not websitetest.username: + username_tag = ( + self.passwords_tree.find( + ".//*[@name='%s']/username" % websitetest.name)) + if username_tag.text: + websitetest.username = username_tag.text + if not websitetest.password: + password_tag = ( + self.passwords_tree.find( + ".//*[@name='%s']/password" % websitetest.name)) + if password_tag.text: + websitetest.password = password_tag.text + self.websitetests.append(websitetest) + if not disabled: + self.working_tests.append(websitetest.name) + + def RemoveAllPasswords(self): + """Removes all the stored passwords.""" + logging.info("\nRemoveAllPasswords\n") + self.driver.get("chrome://settings/passwords") + self.driver.switch_to_frame("settings") + while True: + try: + self.driver.execute_script("document.querySelector('" + "#saved-passwords-list .row-delete-button').click()") + time.sleep(1) + except NoSuchElementException: + break + except WebDriverException: + break + + def OpenTabAndGoToInternals(self, url): + """If there is no |self.website_window|, opens a new tab and navigates to + |url| in the new tab. Navigates to the passwords internals page in the + first tab. Raises an exception otherwise. + + Args: + url: Url to go to in the new tab. + + Raises: + Exception: An exception is raised if |self.website_window| already + exists. + """ + if self.website_window: + raise Exception("Error: The window was already opened.") + + self.driver.get("chrome://newtab") + # There is no straightforward way to open a new tab with chromedriver. + # One work-around is to go to a website, insert a link that is going + # to be opened in a new tab, click on it. + a = self.driver.execute_script( + "var a = document.createElement('a');" + "a.target = '_blank';" + "a.href = arguments[0];" + "a.innerHTML = '.';" + "document.body.appendChild(a);" + "return a;", + url) + + a.click() + time.sleep(1) + + self.website_window = self.driver.window_handles[-1] + self.driver.get(self.internals_page) + self.driver.switch_to_window(self.website_window) + + def SwitchToInternals(self): + """Switches from the Website window to internals tab.""" + self.driver.switch_to_window(self.internals_window) + + def SwitchFromInternals(self): + """Switches from internals tab to the Website window.""" + self.driver.switch_to_window(self.website_window) + + def _DidMessageAppearUntilTimeout(self, log_message, timeout): + """Checks whether the save password prompt is shown. + + Args: + log_message: Log message to look for in the password internals. + timeout: There is some delay between the login and the password + internals update. The method checks periodically during the first + |timeout| seconds if the internals page reports the prompt being + shown. If the prompt is not reported shown within the first + |timeout| seconds, it is considered not shown at all. + + Returns: + True if the save password prompt is shown. + False otherwise. + """ + log = self.driver.find_element_by_css_selector("#log-entries") + count = log.text.count(log_message) + + if count > self.message_count[log_message]: + self.message_count[log_message] = count + return True + elif timeout > 0: + time.sleep(1) + return self._DidMessageAppearUntilTimeout(log_message, timeout - 1) + else: + return False + + def CheckForNewMessage(self, log_message, message_should_show_up, + error_message, timeout=3): + """Detects whether the save password prompt is shown. + + Args: + log_message: Log message to look for in the password internals. The + only valid values are the constants MESSAGE_* defined at the + beginning of this file. + message_should_show_up: Whether or not the message is expected to be + shown. + error_message: Error message for the exception. + timeout: There is some delay between the login and the password + internals update. The method checks periodically during the first + |timeout| seconds if the internals page reports the prompt being + shown. If the prompt is not reported shown within the first + |timeout| seconds, it is considered not shown at all. + + Raises: + Exception: An exception is raised in case the result does not match the + expectation + """ + if (self._DidMessageAppearUntilTimeout(log_message, timeout) != + message_should_show_up): + raise Exception(error_message) + + def AllTests(self, prompt_test): + """Runs the tests on all the WebsiteTests. + + Args: + prompt_test: If True, tests caring about showing the save-password + prompt are going to be run, otherwise tests which don't care about + the prompt are going to be run. + + Raises: + Exception: An exception is raised if the tests fail. + """ + if prompt_test: + self.PromptTestList(self.websitetests) + else: + self.TestList(self.websitetests) + + def WorkingTests(self, prompt_test): + """Runs the tests on all the enabled WebsiteTests. + + Args: + prompt_test: If True, tests caring about showing the save-password + prompt are going to be run, otherwise tests which don't care about + the prompt are going to be executed. + + Raises: + Exception: An exception is raised if the tests fail. + """ + self.Test(self.working_tests, prompt_test) + + def Test(self, tests, prompt_test): + """Runs the tests on websites named in |tests|. + + Args: + tests: A list of the names of the WebsiteTests that are going to be + tested. + prompt_test: If True, tests caring about showing the save-password + prompt are going to be run, otherwise tests which don't care about + the prompt are going to be executed. + + Raises: + Exception: An exception is raised if the tests fail. + """ + websitetests = [] + for websitetest in self.websitetests: + if websitetest.name in tests: + websitetests.append(websitetest) + + if prompt_test: + self.PromptTestList(websitetests) + else: + self.TestList(websitetests) + + def TestList(self, websitetests): + """Runs the tests on the websites in |websitetests|. + + Args: + websitetests: A list of WebsiteTests that are going to be tested. + + Raises: + Exception: An exception is raised if the tests fail. + """ + self.RemoveAllPasswords() + + for websitetest in websitetests: + websitetest.WrongLoginTest() + websitetest.SuccessfulLoginTest() + websitetest.SuccessfulLoginWithAutofilledPasswordTest() + + self.RemoveAllPasswords() + for websitetest in websitetests: + websitetest.SuccessfulLoginTest() + + def PromptTestList(self, websitetests): + """Runs the prompt tests on the websites in |websitetests|. + + Args: + websitetests: A list of WebsiteTests that are going to be tested. + + Raises: + Exception: An exception is raised if the tests fail. + """ + self.RemoveAllPasswords() + + for websitetest in websitetests: + websitetest.PromptTest() + + def Quit(self): + """Closes the tests.""" + # Close the webdriver. + self.driver.quit() diff --git a/components/test/data/password_manager/tests.py b/components/test/data/password_manager/tests.py new file mode 100644 index 0000000..af9f44e --- /dev/null +++ b/components/test/data/password_manager/tests.py @@ -0,0 +1,515 @@ +# Copyright 2014 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. + +# -*- coding: utf-8 -*- +"""Automated tests for many websites""" + +import argparse +import logging + +from environment import Environment +from websitetest import WebsiteTest + + +class Facebook(WebsiteTest): + + def Login(self): + self.GoTo("https://www.facebook.com") + self.FillUsernameInto("[name='email']") + self.FillPasswordInto("[name='pass']") + self.Submit("[name='pass']") + + def Logout(self): + self.WaitUntilDisplayed("#userNavigationLabel") + self.Click("#userNavigationLabel") + self.WaitUntilDisplayed("#logout_form [type='submit']") + self.Click("#logout_form [type='submit']") + + +class Google(WebsiteTest): + + def Login(self): + self.GoTo("https://accounts.google.com/ServiceLogin?sacu=1&continue=") + self.FillUsernameInto("#Email") + self.FillPasswordInto("#Passwd") + self.Submit("#Passwd") + + def Logout(self): + self.GoTo("https://accounts.google.com/Logout") + + +class Linkedin(WebsiteTest): + + def Login(self): + self.GoTo("https://www.linkedin.com") + self.FillUsernameInto("#session_key-login") + self.FillPasswordInto("#session_password-login") + self.Submit("#session_password-login") + + def Logout(self): + self.WaitUntilDisplayed(".account-toggle") + self.HoverOver(".account-toggle") + self.WaitUntilDisplayed(".account-settings .act-set-action") + self.Click(".account-settings .act-set-action") + + +class Mailru(WebsiteTest): + + def Login(self): + self.GoTo("https://mail.ru") + self.FillUsernameInto("#mailbox__login") + self.FillPasswordInto("#mailbox__password") + self.Submit("#mailbox__password") + + def Logout(self): + self.Click("#PH_logoutLink") + + +class Nytimes(WebsiteTest): + + def Login(self): + self.GoTo("https://myaccount.nytimes.com/auth/login") + self.FillUsernameInto("#userid") + self.FillPasswordInto("#password") + self.Submit("#password") + + def Logout(self): + self.GoTo("https://myaccount.nytimes.com/gst/signout") + + +class Pinterest(WebsiteTest): + + def Login(self): + self.GoTo("https://www.pinterest.com/login/") + self.FillUsernameInto("[name='username_or_email']") + self.FillPasswordInto("[name='password']") + self.Submit("[name='password']") + + def Logout(self): + self.GoTo("https://www.pinterest.com/logout/") + + +class Reddit(WebsiteTest): + + def Login(self): + self.GoTo("http://www.reddit.com") + self.Click(".user .login-required") + self.FillUsernameInto("#user_login") + self.FillPasswordInto("#passwd_login") + self.Wait(2) + self.Submit("#passwd_login") + + def Logout(self): + self.Click("form[action='http://www.reddit.com/logout'] a") + + +class Tumblr(WebsiteTest): + + def Login(self): + self.GoTo("https://www.tumblr.com/login") + self.FillUsernameInto("#signup_email") + self.FillPasswordInto("#signup_password") + self.Submit("#signup_password") + + def Logout(self): + self.GoTo("https://www.tumblr.com/logout") + + +class Wikipedia(WebsiteTest): + + def Login(self): + self.GoTo("https://en.wikipedia.org/w/index.php?title=Special:UserLogin") + self.FillUsernameInto("#wpName1") + self.FillPasswordInto("#wpPassword1") + self.Submit("#wpPassword1") + + def Logout(self): + self.GoTo("https://en.wikipedia.org/w/index.php?title=Special:UserLogout") + + +class Yandex(WebsiteTest): + + def Login(self): + self.GoTo("https://mail.yandex.com") + self.FillUsernameInto("#b-mail-domik-username11") + self.FillPasswordInto("#b-mail-domik-password11") + self.Click(".b-mail-button__button") + + def Logout(self): + while not self.IsDisplayed(".b-mail-dropdown__item__content" + u".Выход.daria-action"): + self.ClickIfClickable(".header-user-pic.b-mail-dropdown__handle") + self.Wait(1) + self.Click(u".b-mail-dropdown__item__content.Выход.daria-action") + + +# Disabled tests. + + +# Bug not reproducible without test. +class Amazon(WebsiteTest): + + def Login(self): + self.GoTo( + "https://www.amazon.com/ap/signin?openid.assoc_handle=usflex" + "&openid.mode=checkid_setup&openid.ns=http%3A%2F%2Fspecs.openid.net" + "%2Fauth%2F2.0") + self.FillUsernameInto("[name='email']") + self.FillPasswordInto("[name='password']") + self.Submit("[name='password']") + + def Logout(self): + while not self.IsDisplayed("#nav-item-signout"): + self.Wait(1) + self.HoverOver("#nav-signin-title") + self.Click("#nav-item-signout") + + +# Password not saved. +class Ask(WebsiteTest): + + def Login(self): + self.GoTo("http://www.ask.com/answers/browse?qsrc=321&q=&o=0&l=dir#") + while not self.IsDisplayed("[name='username']"): + self.Click("#a16CnbSignInText") + self.Wait(1) + self.FillUsernameInto("[name='username']") + self.FillPasswordInto("[name='password']") + self.Click(".signin_show.signin_submit") + + def Logout(self): + self.WaitUntilDisplayed("#a16CnbSignInText") + self.Click("#a16CnbSignInText") + + +# Password not saved. +class Baidu(WebsiteTest): + + def Login(self): + self.GoTo("http://www.baidu.com/") + self.Click("[name='tj_login']") + self.WaitUntilDisplayed("[name='userName']") + self.FillUsernameInto("[name='userName']") + self.FillPasswordInto("[name='password']") + self.Submit("[name='password']") + + def Logout(self): + self.Wait(1) + self.GoTo("https://passport.baidu.com/?logout&u=http://www.baidu.com") + + +# http://crbug.com/368690 +class Cnn(WebsiteTest): + + def Login(self): + self.GoTo("http://www.cnn.com") + self.Wait(5) + while not self.IsDisplayed(".cnnOvrlyBtn.cnnBtnLogIn"): + self.ClickIfClickable("#hdr-auth .no-border.no-pad-right a") + self.Wait(1) + + self.Click(".cnnOvrlyBtn.cnnBtnLogIn") + self.FillUsernameInto("#cnnOverlayEmail1l") + self.FillPasswordInto("#cnnOverlayPwd") + self.Click(".cnnOvrlyBtn.cnnBtnLogIn") + self.Click(".cnnOvrlyBtn.cnnBtnLogIn") + self.Wait(5) + + def Logout(self): + self.Wait(4) + self.Click("#hdr-auth .no-border.no-pad-right") + + +# http://crbug.com/368690 +class Ebay(WebsiteTest): + + def Login(self): + self.GoTo("https://signin.ebay.com/") + self.FillUsernameInto("[name='userid']") + self.FillPasswordInto("[name='pass']") + self.Submit("[name='pass']") + + def Logout(self): + self.WaitUntilDisplayed("#gh-ug") + self.Click("#gh-ug") + self.WaitUntilDisplayed("#gh-uo") + self.Click("#gh-uo") + + +# Iframe, password saved but not autofileld. +class Espn(WebsiteTest): + + def Login(self): + self.GoTo("http://espn.go.com/") + while not self.IsDisplayed("#cboxLoadedContent iframe"): + self.Click("#signin .cbOverlay") + self.Wait(1) + frame = self.driver.find_element_by_css_selector("#cboxLoadedContent " + "iframe") + self.driver.switch_to_frame(frame) + self.WaitUntilDisplayed("#username") + self.FillUsernameInto("#username") + self.FillPasswordInto("#password") + while self.IsDisplayed("#password"): + self.ClickIfClickable("#submitBtn") + self.Wait(1) + + def Logout(self): + self.WaitUntilDisplayed("#signin .small") + self.Click("#signin .small") + + +# http://crbug.com/367768 +class Live(WebsiteTest): + + def Login(self): + self.GoTo("https://www.live.com") + self.FillUsernameInto("[name='login']") + self.FillPasswordInto("[name='passwd']") + self.Submit("[name='passwd']") + + def Logout(self): + self.WaitUntilDisplayed("#c_meun") + self.Click("#c_meun") + self.WaitUntilDisplayed("#c_signout") + self.Click("#c_signout") + + +# http://crbug.com/368690 +class One63(WebsiteTest): + + def Login(self): + self.GoTo("http://www.163.com") + self.HoverOver("#js_N_navHighlight") + self.WaitUntilDisplayed("#js_loginframe_username") + self.FillUsernameInto("#js_loginframe_username") + self.FillPasswordInto(".ntes-loginframe-label-ipt[type='password']") + self.Click(".ntes-loginframe-btn") + + def Logout(self): + self.WaitUntilDisplayed("#js_N_navLogout") + self.Click("#js_N_navLogout") + + +# http://crbug.com/368690 +class Vube(WebsiteTest): + + def Login(self): + self.GoTo("https://vube.com") + self.Click("[vube-login='']") + self.WaitUntilDisplayed("[ng-model='login.user']") + self.FillUsernameInto("[ng-model='login.user']") + self.FillPasswordInto("[ng-model='login.pass']") + while (self.IsDisplayed("[ng-model='login.pass']") + and not self.IsDisplayed(".prompt.alert")): + self.ClickIfClickable("[ng-click='login()']") + self.Wait(1) + + def Logout(self): + self.WaitUntilDisplayed("[ng-click='user.logout()']") + self.Click("[ng-click='user.logout()']") + + +# Tests that can cause a crash. + + +class Yahoo(WebsiteTest): + + def Login(self): + self.GoTo("https://login.yahoo.com") + self.FillUsernameInto("#username") + self.FillPasswordInto("#passwd") + self.Submit("#passwd") + + def Logout(self): + self.WaitUntilDisplayed(".tab.tab-user>.mod.view_default") + self.HoverOver(".tab.tab-user>.mod.view_default") + self.WaitUntilDisplayed("[data-pos='4'] .lbl.y-link-1") + self.Click("[data-pos='4'] .lbl.y-link-1") + + +def Tests(environment): + + + # Working tests. + + + environment.AddWebsiteTest(Facebook("facebook")) + + environment.AddWebsiteTest(Google("google")) + + environment.AddWebsiteTest(Linkedin("linkedin")) + + environment.AddWebsiteTest(Mailru("mailru")) + + environment.AddWebsiteTest(Nytimes("nytimes")) + + environment.AddWebsiteTest(Pinterest("pinterest")) + + environment.AddWebsiteTest(Reddit("reddit", username_not_auto=True)) + + environment.AddWebsiteTest(Tumblr("tumblr", username_not_auto=True)) + + environment.AddWebsiteTest(Wikipedia("wikipedia", username_not_auto=True)) + + environment.AddWebsiteTest(Yandex("yandex")) + + + # Disabled tests. + + + # Bug not reproducible without test. + environment.AddWebsiteTest(Amazon("amazon"), disabled=True) + + # Password not saved. + environment.AddWebsiteTest(Ask("ask"), disabled=True) + + # Password not saved. + environment.AddWebsiteTest(Baidu("baidu"), disabled=True) + + # http://crbug.com/368690 + environment.AddWebsiteTest(Cnn("cnn"), disabled=True) + + # http://crbug.com/368690 + environment.AddWebsiteTest(Ebay("ebay"), disabled=True) + + # Iframe, password saved but not autofileld. + environment.AddWebsiteTest(Espn("espn"), disabled=True) + + # http://crbug.com/367768 + environment.AddWebsiteTest(Live("live", username_not_auto=True), + disabled=True) + + # http://crbug.com/368690 + environment.AddWebsiteTest(One63("163"), disabled=True) + + # http://crbug.com/368690 + environment.AddWebsiteTest(Vube("vube"), disabled=True) + + # Tests that can cause a crash (the cause of the crash is not related to the + # password manager). + environment.AddWebsiteTest(Yahoo("yahoo", username_not_auto=True), + disabled=True) + + +def RunTests(chrome_path, chromedriver_path, profile_path, + environment_passwords_path, enable_automatic_password_saving, + environment_numeric_level, log_to_console, environment_log_file, + all_tests, tests): + + """Runs the the tests + + Args: + chrome_path: The chrome binary file. + chromedriver_path: The chromedriver binary file. + profile_path: The chrome testing profile folder. + environment_passwords_path: The usernames and passwords file. + enable_automatic_password_saving: If True, the passwords are going to be + saved without showing the prompt. + environment_numeric_level: The log verbosity. + log_to_console: If True, the debug logs will be shown on the console. + environment_log_file: The file where to store the log. If it's empty, the + log is not stored. + all_tests: If True, all the tests are going to be ran. + tests: A list of the names of the WebsiteTests that are going to be tested. + + Raises: + Exception: An exception is raised if the one of the tests fails. + """ + + environment = Environment(chrome_path, chromedriver_path, profile_path, + environment_passwords_path, + enable_automatic_password_saving, + environment_numeric_level, + log_to_console, + environment_log_file) + + # Test which care about the save-password prompt need the prompt + # to be shown. Automatic password saving results in no prompt. + run_prompt_tests = not enable_automatic_password_saving + + Tests(environment) + + if all_tests: + environment.AllTests(run_prompt_tests) + elif tests: + environment.Test(tests, run_prompt_tests) + else: + environment.WorkingTests(run_prompt_tests) + + environment.Quit() + + +# Tests setup. +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Password Manager automated tests help.") + + parser.add_argument( + "--chrome-path", action="store", dest="chrome_path", + help="Set the chrome path (required).", nargs=1, required=True) + parser.add_argument( + "--chromedriver-path", action="store", dest="chromedriver_path", + help="Set the chromedriver path (required).", nargs=1, required=True) + parser.add_argument( + "--profile-path", action="store", dest="profile_path", + help="Set the profile path (required). You just need to choose a " + "temporary empty folder. If the folder is not empty all its content " + "is going to be removed.", + nargs=1, required=True) + + parser.add_argument( + "--passwords-path", action="store", dest="passwords_path", + help="Set the usernames/passwords path (required).", nargs=1, + required=True) + parser.add_argument("--all", action="store_true", dest="all", + help="Run all tests.") + parser.add_argument("--log", action="store", nargs=1, dest="log_level", + help="Set log level.") + + parser.add_argument("--log-screen", action="store_true", dest="log_screen", + help="Show log on the screen.") + parser.add_argument("--log-file", action="store", dest="log_file", + help="Write the log in a file.", nargs=1) + parser.add_argument("tests", help="Tests to be run.", nargs="*") + + args = parser.parse_args() + + passwords_path = args.passwords_path[0] + + numeric_level = None + if args.log_level: + numeric_level = getattr(logging, args.log_level[0].upper(), None) + if not isinstance(numeric_level, int): + raise ValueError("Invalid log level: %s" % args.log_level[0]) + + log_file = None + if args.log_file: + log_file = args.log_file[0] + + # Run the test without enable-automatic-password-saving to check whether or + # not the prompt is shown in the way we expected. + RunTests(args.chrome_path[0], + args.chromedriver_path[0], + args.profile_path[0], + passwords_path, + False, + numeric_level, + args.log_screen, + log_file, + args.all, + args.tests) + + # Run the test with enable-automatic-password-saving to check whether or not + # the passwords is stored in the the way we expected. + RunTests(args.chrome_path[0], + args.chromedriver_path[0], + args.profile_path[0], + passwords_path, + True, + numeric_level, + args.log_screen, + log_file, + args.all, + args.tests) diff --git a/components/test/data/password_manager/websitetest.py b/components/test/data/password_manager/websitetest.py new file mode 100644 index 0000000..a0df593 --- /dev/null +++ b/components/test/data/password_manager/websitetest.py @@ -0,0 +1,406 @@ +# Copyright 2014 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. + +"""WebsiteTest testing class.""" + +import logging +import time + +from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.common.keys import Keys + +import environment + + +def _IsOneSubstringOfAnother(s1, s2): + """Checks if one of the string arguements is substring of the other. + + Args: + s1: The first string. + s2: The second string. + Returns: + + True if one of the string arguements is substring of the other. + False otherwise. + """ + return s1 in s2 or s2 in s1 + + +class WebsiteTest: + """Handles a tested WebsiteTest.""" + + class Mode: + """Test mode.""" + # Password and username are expected to be autofilled. + AUTOFILLED = 1 + # Password and username are not expected to be autofilled. + NOT_AUTOFILLED = 2 + + def __init__(self): + pass + + def __init__(self, name, username_not_auto=False): + """Creates a new WebsiteTest. + + Args: + name: The website name. + username_not_auto: Username inputs in some websites (like wikipedia) are + sometimes filled with some messages and thus, the usernames are not + automatically autofilled. This flag handles that and disables us from + checking if the state of the DOM is the same as the username of + website. + """ + # Name of the website + self.name = name + # Username of the website. + self.username = None + # Password of the website. + self.password = None + # Username is not automatically filled. + self.username_not_auto = username_not_auto + # Autofilling mode. + self.mode = self.Mode.NOT_AUTOFILLED + # The |remaining_time_to_wait| limits the total time in seconds spent in + # potentially infinite loops. + self.remaining_time_to_wait = 200 + # The testing Environment. + self.environment = None + # The webdriver. + self.driver = None + + # Mouse/Keyboard actions. + + def Click(self, selector): + """Clicks on an element. + + Args: + selector: The element CSS selector. + """ + logging.info("action: Click %s" % selector) + element = self.driver.find_element_by_css_selector(selector) + element.click() + + def ClickIfClickable(self, selector): + """Clicks on an element if it's clickable: If it doesn't exist in the DOM, + it's covered by another element or it's out viewing area, nothing is + done and False is returned. Otherwise, even if the element is 100% + transparent, the element is going to receive a click and a True is + returned. + + Args: + selector: The element CSS selector. + + Returns: + True if the click happens. + False otherwise. + """ + logging.info("action: ClickIfVisible %s" % selector) + try: + element = self.driver.find_element_by_css_selector(selector) + element.click() + return True + except Exception: + return False + + def GoTo(self, url): + """Navigates the main frame to the |url|. + + Args: + url: The URL. + """ + logging.info("action: GoTo %s" % self.name) + if self.environment.first_go_to: + self.environment.OpenTabAndGoToInternals(url) + self.environment.first_go_to = False + else: + self.driver.get(url) + + def HoverOver(self, selector): + """Hovers over an element. + + Args: + selector: The element CSS selector. + """ + logging.info("action: Hover %s" % selector) + element = self.driver.find_element_by_css_selector(selector) + hover = ActionChains(self.driver).move_to_element(element) + hover.perform() + + def SendEnterTo(self, selector): + """Sends an enter key to an element. + + Args: + selector: The element CSS selector. + """ + logging.info("action: SendEnterTo %s" % selector) + body = self.driver.find_element_by_tag_name("body") + body.send_keys(Keys.ENTER) + + # Waiting/Displaying actions. + + def IsDisplayed(self, selector): + """Returns False if an element doesn't exist in the DOM or is 100% + transparent. Otherwise, returns True even if it's covered by another + element or it's out viewing area. + + Args: + selector: The element CSS selector. + """ + logging.info("action: IsDisplayed %s" % selector) + try: + element = self.driver.find_element_by_css_selector(selector) + return element.is_displayed() + except Exception: + return False + + def Wait(self, duration): + """Wait for a duration in seconds. This needs to be used in potentially + infinite loops, to limit their running time. + + Args: + duration: The time to wait in seconds. + """ + logging.info("action: Wait %s" % duration) + time.sleep(duration) + self.remaining_time_to_wait -= 1 + if self.remaining_time_to_wait < 0: + raise Exception("Tests took more time than expected for the following " + "website : %s \n" % self.name) + + def WaitUntilDisplayed(self, selector, timeout=10): + """Waits until an element is displayed. + + Args: + selector: The element CSS selector. + timeout: The maximum waiting time in seconds before failing. + """ + if not self.IsDisplayed(selector): + self.Wait(1) + timeout = timeout - 1 + if (timeout <= 0): + raise Exception("Error: Element %s not shown before timeout is " + "finished for the following website: %s" + % (selector, self.name)) + else: + self.WaitUntilDisplayed(selector, timeout) + + # Form actions. + + def FillPasswordInto(self, selector): + """If the testing mode is the Autofilled mode, compares the website + password to the DOM state. + If the testing mode is the NotAutofilled mode, checks that the DOM state + is empty. + Then, fills the input with the Website password. + + Args: + selector: The password input CSS selector. + + Raises: + Exception: An exception is raised if the DOM value of the password is + different than the one we expected. + """ + logging.info("action: FillPasswordInto %s" % selector) + + password_element = self.driver.find_element_by_css_selector(selector) + # Chrome protects the password inputs and doesn't fill them until + # the user interacts with the page. To be sure that such thing has + # happened we click on the password fields or one of its ancestors. + element = password_element + while True: + try: + element.click() + break + except Exception: + try: + element = element.parent + except AttributeError: + raise Exception("Error: unable to find a clickable element to " + "release the password protection for the following website: %s \n" + % (self.name)) + + if self.mode == self.Mode.AUTOFILLED: + autofilled_password = password_element.get_attribute("value") + if autofilled_password != self.password: + raise Exception("Error: autofilled password is different from the one " + "we just saved for the following website : %s p1: %s " + "p2:%s \n" % (self.name, + password_element.get_attribute("value"), + self.password)) + + elif self.mode == self.Mode.NOT_AUTOFILLED: + autofilled_password = password_element.get_attribute("value") + if autofilled_password: + raise Exception("Error: password is autofilled when it shouldn't be " + "for the following website : %s \n" + % self.name) + + password_element.send_keys(self.password) + + def FillUsernameInto(self, selector): + """If the testing mode is the Autofilled mode, compares the website + username to the input value. Then, fills the input with the website + username. + + Args: + selector: The username input CSS selector. + + Raises: + Exception: An exception is raised if the DOM value of the username is + different that the one we expected. + """ + logging.info("action: FillUsernameInto %s" % selector) + username_element = self.driver.find_element_by_css_selector(selector) + + if (self.mode == self.Mode.AUTOFILLED and not self.username_not_auto): + if not (username_element.get_attribute("value") == self.username): + raise Exception("Error: autofilled username is different form the one " + "we just saved for the following website : %s \n" % + self.name) + else: + username_element.clear() + username_element.send_keys(self.username) + + def Submit(self, selector): + """Finds an element using CSS Selector and calls its submit() handler. + + Args: + selector: The input CSS selector. + """ + logging.info("action: Submit %s" % selector) + element = self.driver.find_element_by_css_selector(selector) + element.submit() + + # Login/Logout Methods + + def Login(self): + """Login Method. Has to be overloaded by the WebsiteTest test.""" + raise NotImplementedError("Login is not implemented.") + + def LoginWhenAutofilled(self): + """Logs in and checks that the password is autofilled.""" + self.mode = self.Mode.AUTOFILLED + self.Login() + + def LoginWhenNotAutofilled(self): + """Logs in and checks that the password is not autofilled.""" + self.mode = self.Mode.NOT_AUTOFILLED + self.Login() + + def Logout(self): + """Logout Method. Has to be overloaded by the Website test.""" + raise NotImplementedError("Logout is not implemented.") + + # Tests + + def WrongLoginTest(self): + """Does the wrong login test: Tries to login with a wrong password and + checks that the password is not saved. + + Raises: + Exception: An exception is raised if the test fails: If there is a + problem when performing the login (ex: the login button is not + available ...), if the state of the username and password fields is + not like we expected or if the password is saved. + """ + logging.info("\nWrong Login Test for %s \n" % self.name) + correct_password = self.password + self.password = self.password + "1" + self.LoginWhenNotAutofilled() + self.password = correct_password + self.Wait(2) + self.environment.SwitchToInternals() + self.environment.CheckForNewMessage( + environment.MESSAGE_SAVE, + False, + "Error: password manager thinks that a login with wrong password was " + "successful for the following website : %s \n" % self.name) + self.environment.SwitchFromInternals() + + def SuccessfulLoginTest(self): + """Does the successful login when the password is not expected to be + autofilled test: Checks that the password is not autofilled, tries to login + with a right password and checks if the password is saved. Then logs out. + + Raises: + Exception: An exception is raised if the test fails: If there is a + problem when performing the login and the logout (ex: the login + button is not available ...), if the state of the username and + password fields is not like we expected or if the password is not + saved. + """ + logging.info("\nSuccessful Login Test for %s \n" % self.name) + self.LoginWhenNotAutofilled() + self.Wait(2) + self.environment.SwitchToInternals() + self.environment.CheckForNewMessage( + environment.MESSAGE_SAVE, + True, + "Error: password manager hasn't detected a successful login for the " + "following website : %s \n" + % self.name) + self.environment.SwitchFromInternals() + self.Logout() + + def SuccessfulLoginWithAutofilledPasswordTest(self): + """Does the successful login when the password is expected to be autofilled + test: Checks that the password is autofilled, tries to login with the + autofilled password and checks if the password is saved. Then logs out. + + Raises: + Exception: An exception is raised if the test fails: If there is a + problem when performing the login and the logout (ex: the login + button is not available ...), if the state of the username and + password fields is not like we expected or if the password is not + saved. + """ + logging.info("\nSuccessful Login With Autofilled Password" + " Test %s \n" % self.name) + self.LoginWhenAutofilled() + self.Wait(2) + self.environment.SwitchToInternals() + self.environment.CheckForNewMessage( + environment.MESSAGE_SAVE, + True, + "Error: password manager hasn't detected a successful login for the " + "following website : %s \n" + % self.name) + self.environment.SwitchFromInternals() + self.Logout() + + def PromptTest(self): + """Does the prompt test: Tries to login with a wrong password and + checks that the prompt is not shown. Then tries to login with a right + password and checks that the prompt is not shown. + + Raises: + Exception: An exception is raised if the test fails: If there is a + problem when performing the login (ex: the login button is not + available ...), if the state of the username and password fields is + not like we expected or if the prompt is not shown for the right + password or is shown for a wrong one. + """ + logging.info("\nPrompt Test for %s \n" % self.name) + correct_password = self.password + self.password = self.password + "1" + self.LoginWhenNotAutofilled() + self.password = correct_password + self.Wait(2) + self.environment.SwitchToInternals() + self.environment.CheckForNewMessage( + environment.MESSAGE_ASK, + False, + "Error: password manager thinks that a login with wrong password was " + "successful for the following website : %s \n" % self.name) + self.environment.SwitchFromInternals() + + self.LoginWhenNotAutofilled() + self.Wait(2) + self.environment.SwitchToInternals() + self.environment.CheckForNewMessage( + environment.MESSAGE_ASK, + True, + "Error: password manager thinks that a login with wrong password was " + "successful for the following website : %s \n" % self.name) + self.environment.SwitchFromInternals() |