1# Copyright 2014 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""The testing Environment class.""" 6 7import logging 8import shutil 9import time 10 11from selenium import webdriver 12from selenium.common.exceptions import NoSuchElementException 13from selenium.common.exceptions import WebDriverException 14from selenium.webdriver.chrome.options import Options 15from xml.etree import ElementTree 16 17# Message strings to look for in chrome://password-manager-internals 18MESSAGE_ASK = "Message: Decision: ASK the user" 19MESSAGE_SAVE = "Message: Decision: SAVE the password" 20 21 22class Environment: 23 """Sets up the testing Environment. """ 24 25 def __init__(self, chrome_path, chromedriver_path, profile_path, 26 passwords_path, enable_automatic_password_saving, 27 numeric_level=None, log_to_console=False, log_file=""): 28 """Creates a new testing Environment. 29 30 Args: 31 chrome_path: The chrome binary file. 32 chromedriver_path: The chromedriver binary file. 33 profile_path: The chrome testing profile folder. 34 passwords_path: The usernames and passwords file. 35 enable_automatic_password_saving: If True, the passwords are going to be 36 saved without showing the prompt. 37 numeric_level: The log verbosity. 38 log_to_console: If True, the debug logs will be shown on the console. 39 log_file: The file where to store the log. If it's empty, the log will 40 not be stored. 41 42 Raises: 43 Exception: An exception is raised if |profile_path| folder could not be 44 removed. 45 """ 46 # Setting up the login. 47 if numeric_level is not None: 48 if log_file: 49 # Set up logging to file. 50 logging.basicConfig(level=numeric_level, 51 filename=log_file, 52 filemode='w') 53 54 if log_to_console: 55 console = logging.StreamHandler() 56 console.setLevel(numeric_level) 57 # Add the handler to the root logger. 58 logging.getLogger('').addHandler(console) 59 60 elif log_to_console: 61 logging.basicConfig(level=numeric_level) 62 63 # Cleaning the chrome testing profile folder. 64 try: 65 shutil.rmtree(profile_path) 66 except Exception, e: 67 # The tests execution can continue, but this make them less stable. 68 logging.error("Error: Could not wipe the chrome profile directory (%s). \ 69 This affects the stability of the tests. Continuing to run tests." 70 % e) 71 options = Options() 72 if enable_automatic_password_saving: 73 options.add_argument("enable-automatic-password-saving") 74 # Chrome path. 75 options.binary_location = chrome_path 76 # Chrome testing profile path. 77 options.add_argument("user-data-dir=%s" % profile_path) 78 79 # The webdriver. It's possible to choose the port the service is going to 80 # run on. If it's left to 0, a free port will be found. 81 self.driver = webdriver.Chrome(chromedriver_path, 0, options) 82 # The password internals window. 83 self.internals_window = self.driver.current_window_handle 84 # Password internals page. 85 self.internals_page = "chrome://password-manager-internals/" 86 # The Website window. 87 self.website_window = None 88 # The WebsiteTests list. 89 self.websitetests = [] 90 # An xml tree filled with logins and passwords. 91 self.passwords_tree = ElementTree.parse(passwords_path).getroot() 92 # The enabled WebsiteTests list. 93 self.working_tests = [] 94 # Map messages to the number of their appearance in the log. 95 self.message_count = dict() 96 self.message_count[MESSAGE_ASK] = 0 97 self.message_count[MESSAGE_SAVE] = 0 98 # The tests needs two tabs to work. A new tab is opened with the first 99 # GoTo. This is why we store here whether or not it's the first time to 100 # execute GoTo. 101 self.first_go_to = True 102 103 def AddWebsiteTest(self, websitetest, disabled=False): 104 """Adds a WebsiteTest to the testing Environment. 105 106 Args: 107 websitetest: The WebsiteTest instance to be added. 108 disabled: Whether test is disabled. 109 """ 110 websitetest.environment = self 111 websitetest.driver = self.driver 112 if self.passwords_tree is not None: 113 if not websitetest.username: 114 username_tag = ( 115 self.passwords_tree.find( 116 ".//*[@name='%s']/username" % websitetest.name)) 117 if username_tag.text: 118 websitetest.username = username_tag.text 119 if not websitetest.password: 120 password_tag = ( 121 self.passwords_tree.find( 122 ".//*[@name='%s']/password" % websitetest.name)) 123 if password_tag.text: 124 websitetest.password = password_tag.text 125 self.websitetests.append(websitetest) 126 if not disabled: 127 self.working_tests.append(websitetest.name) 128 129 def RemoveAllPasswords(self): 130 """Removes all the stored passwords.""" 131 logging.info("\nRemoveAllPasswords\n") 132 self.driver.get("chrome://settings/passwords") 133 self.driver.switch_to_frame("settings") 134 while True: 135 try: 136 self.driver.execute_script("document.querySelector('" 137 "#saved-passwords-list .row-delete-button').click()") 138 time.sleep(1) 139 except NoSuchElementException: 140 break 141 except WebDriverException: 142 break 143 144 def OpenTabAndGoToInternals(self, url): 145 """If there is no |self.website_window|, opens a new tab and navigates to 146 |url| in the new tab. Navigates to the passwords internals page in the 147 first tab. Raises an exception otherwise. 148 149 Args: 150 url: Url to go to in the new tab. 151 152 Raises: 153 Exception: An exception is raised if |self.website_window| already 154 exists. 155 """ 156 if self.website_window: 157 raise Exception("Error: The window was already opened.") 158 159 self.driver.get("chrome://newtab") 160 # There is no straightforward way to open a new tab with chromedriver. 161 # One work-around is to go to a website, insert a link that is going 162 # to be opened in a new tab, click on it. 163 a = self.driver.execute_script( 164 "var a = document.createElement('a');" 165 "a.target = '_blank';" 166 "a.href = arguments[0];" 167 "a.innerHTML = '.';" 168 "document.body.appendChild(a);" 169 "return a;", 170 url) 171 172 a.click() 173 time.sleep(1) 174 175 self.website_window = self.driver.window_handles[-1] 176 self.driver.get(self.internals_page) 177 self.driver.switch_to_window(self.website_window) 178 179 def SwitchToInternals(self): 180 """Switches from the Website window to internals tab.""" 181 self.driver.switch_to_window(self.internals_window) 182 183 def SwitchFromInternals(self): 184 """Switches from internals tab to the Website window.""" 185 self.driver.switch_to_window(self.website_window) 186 187 def _DidMessageAppearUntilTimeout(self, log_message, timeout): 188 """Checks whether the save password prompt is shown. 189 190 Args: 191 log_message: Log message to look for in the password internals. 192 timeout: There is some delay between the login and the password 193 internals update. The method checks periodically during the first 194 |timeout| seconds if the internals page reports the prompt being 195 shown. If the prompt is not reported shown within the first 196 |timeout| seconds, it is considered not shown at all. 197 198 Returns: 199 True if the save password prompt is shown. 200 False otherwise. 201 """ 202 log = self.driver.find_element_by_css_selector("#log-entries") 203 count = log.text.count(log_message) 204 205 if count > self.message_count[log_message]: 206 self.message_count[log_message] = count 207 return True 208 elif timeout > 0: 209 time.sleep(1) 210 return self._DidMessageAppearUntilTimeout(log_message, timeout - 1) 211 else: 212 return False 213 214 def CheckForNewMessage(self, log_message, message_should_show_up, 215 error_message, timeout=3): 216 """Detects whether the save password prompt is shown. 217 218 Args: 219 log_message: Log message to look for in the password internals. The 220 only valid values are the constants MESSAGE_* defined at the 221 beginning of this file. 222 message_should_show_up: Whether or not the message is expected to be 223 shown. 224 error_message: Error message for the exception. 225 timeout: There is some delay between the login and the password 226 internals update. The method checks periodically during the first 227 |timeout| seconds if the internals page reports the prompt being 228 shown. If the prompt is not reported shown within the first 229 |timeout| seconds, it is considered not shown at all. 230 231 Raises: 232 Exception: An exception is raised in case the result does not match the 233 expectation 234 """ 235 if (self._DidMessageAppearUntilTimeout(log_message, timeout) != 236 message_should_show_up): 237 raise Exception(error_message) 238 239 def AllTests(self, prompt_test): 240 """Runs the tests on all the WebsiteTests. 241 242 Args: 243 prompt_test: If True, tests caring about showing the save-password 244 prompt are going to be run, otherwise tests which don't care about 245 the prompt are going to be run. 246 247 Raises: 248 Exception: An exception is raised if the tests fail. 249 """ 250 if prompt_test: 251 self.PromptTestList(self.websitetests) 252 else: 253 self.TestList(self.websitetests) 254 255 def WorkingTests(self, prompt_test): 256 """Runs the tests on all the enabled WebsiteTests. 257 258 Args: 259 prompt_test: If True, tests caring about showing the save-password 260 prompt are going to be run, otherwise tests which don't care about 261 the prompt are going to be executed. 262 263 Raises: 264 Exception: An exception is raised if the tests fail. 265 """ 266 self.Test(self.working_tests, prompt_test) 267 268 def Test(self, tests, prompt_test): 269 """Runs the tests on websites named in |tests|. 270 271 Args: 272 tests: A list of the names of the WebsiteTests that are going to be 273 tested. 274 prompt_test: If True, tests caring about showing the save-password 275 prompt are going to be run, otherwise tests which don't care about 276 the prompt are going to be executed. 277 278 Raises: 279 Exception: An exception is raised if the tests fail. 280 """ 281 websitetests = [] 282 for websitetest in self.websitetests: 283 if websitetest.name in tests: 284 websitetests.append(websitetest) 285 286 if prompt_test: 287 self.PromptTestList(websitetests) 288 else: 289 self.TestList(websitetests) 290 291 def TestList(self, websitetests): 292 """Runs the tests on the websites in |websitetests|. 293 294 Args: 295 websitetests: A list of WebsiteTests that are going to be tested. 296 297 Raises: 298 Exception: An exception is raised if the tests fail. 299 """ 300 self.RemoveAllPasswords() 301 302 for websitetest in websitetests: 303 websitetest.WrongLoginTest() 304 websitetest.SuccessfulLoginTest() 305 websitetest.SuccessfulLoginWithAutofilledPasswordTest() 306 307 self.RemoveAllPasswords() 308 for websitetest in websitetests: 309 websitetest.SuccessfulLoginTest() 310 311 def PromptTestList(self, websitetests): 312 """Runs the prompt tests on the websites in |websitetests|. 313 314 Args: 315 websitetests: A list of WebsiteTests that are going to be tested. 316 317 Raises: 318 Exception: An exception is raised if the tests fail. 319 """ 320 self.RemoveAllPasswords() 321 322 for websitetest in websitetests: 323 websitetest.PromptTest() 324 325 def Quit(self): 326 """Closes the tests.""" 327 # Close the webdriver. 328 self.driver.quit() 329