summaryrefslogtreecommitdiffstats
path: root/components/test/data/password_manager/environment.py
blob: b3ff7cd38a4385a40ec5c7f1fb243af0555d604a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# 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
import traceback
from xml.etree import ElementTree
from xml.sax.saxutils import escape

from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.chrome.options import Options


# 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)
    # If |chrome_path| is not defined, this means that we are in the dashboard
    # website, and we just need to get the list of all websites. In this case,
    # we don't need to initilize the webdriver.
    if chrome_path:
      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
      if passwords_path:
        # An xml tree filled with logins and passwords.
        self.passwords_tree = ElementTree.parse(passwords_path).getroot()
      else:
        raise Exception("Error: |passwords_path| needs to be provided if"
            "|chrome_path| is provided, otherwise the tests could not be run")
    # Password internals page.
    self.internals_page = "chrome://password-manager-internals/"
    # The Website window.
    self.website_window = None
    # The WebsiteTests list.
    self.websitetests = []
    # 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
    if hasattr(self, "driver"):
      websitetest.driver = self.driver
    if hasattr(self, "passwords_tree") and 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()