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