Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(211)

Side by Side Diff: components/proximity_auth/e2e_test/cros.py

Issue 1004283002: Add end-to-end testing tool for Smart Lock. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: fixes Created 5 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | components/proximity_auth/e2e_test/cryptauth.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright 2015 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 import logging
6 import os
7 import subprocess
8 import sys
9 import time
10
11 # Add the telemetry directory to Python's search paths.
12 current_directory = os.path.dirname(os.path.realpath(__file__))
13 telemetry_dir = os.path.realpath(
14 os.path.join(current_directory, '..', '..', '..', 'tools', 'telemetry'))
15 if telemetry_dir not in sys.path:
16 sys.path.append(telemetry_dir)
17
18 from telemetry.core import browser_options
19 from telemetry.core import browser_finder
20 from telemetry.core import extension_to_load
21 from telemetry.core import exceptions
22 from telemetry.core import util
23 from telemetry.core.platform import cros_interface
24
25 logger = logging.getLogger('proximity_auth.%s' % __name__)
26
27
28 class AccountPickerScreen(object):
29 """ Wrapper for the ChromeOS account picker screen.
30
31 The account picker screen is the WebContents page used for both the lock
32 screen and signin screen.
33
34 Note: This class assumes the account picker screen only has one user. If there
35 are multiple user pods, the first one will be used.
36 """
37
38 class AuthType:
39 """ The authentication type expected for a user pod. """
40 OFFLINE_PASSWORD = 0
41 ONLINE_SIGN_IN = 1
42 NUMERIC_PIN = 2
43 USER_CLICK = 3
44 EXPAND_THEN_USER_CLICK = 4
45 FORCE_OFFLINE_PASSWORD = 5
46
47 class SmartLockState:
48 """ The state of the Smart Lock icon on a user pod.
49 """
50 NOT_SHOWN = 'not_shown'
51 AUTHENTICATED = 'authenticated'
52 LOCKED = 'locked'
53 HARD_LOCKED = 'hardlocked'
54 TO_BE_ACTIVATED = 'to_be_activated'
55 SPINNER = 'spinner'
56
57 # JavaScript expression for getting the user pod on the page
58 _GET_POD_JS = 'document.getElementById("pod-row").pods[0]'
59
60 def __init__(self, oobe, chromeos):
61 """
62 Args:
63 oobe: Inspector page of the OOBE WebContents.
64 chromeos: The parent Chrome wrapper.
65 """
66 self._oobe = oobe
67 self._chromeos = chromeos
68
69 @property
70 def is_lockscreen(self):
71 return self._oobe.EvaluateJavaScript(
72 '!document.getElementById("sign-out-user-item").hidden')
73
74 @property
75 def auth_type(self):
76 return self._oobe.EvaluateJavaScript('%s.authType' % self._GET_POD_JS)
77
78 @property
79 def smart_lock_state(self):
80 icon_shown = self._oobe.EvaluateJavaScript(
81 '!%s.customIconElement.hidden' % self._GET_POD_JS)
82 if not icon_shown:
83 return self.SmartLockState.NOT_SHOWN
84 class_list_dict = self._oobe.EvaluateJavaScript(
85 '%s.customIconElement.querySelector(".custom-icon")'
86 '.classList' % self._GET_POD_JS)
87 class_list = [v for k,v in class_list_dict.items() if k != 'length']
88
89 if 'custom-icon-unlocked' in class_list:
90 return self.SmartLockState.AUTHENTICATED
91 if 'custom-icon-locked' in class_list:
92 return self.SmartLockState.LOCKED
93 if 'custom-icon-hardlocked' in class_list:
94 return self.SmartLockState.HARD_LOCKED
95 if 'custom-icon-locked-to-be-activated' in class_list:
96 return self.SmartLockState.TO_BE_ACTIVATED
97 if 'custom-icon-spinner' in class_list:
98 return self.SmartLockState.SPINNER
99
100 def WaitForSmartLockState(self, state, wait_time_secs=60):
101 """ Waits for the Smart Lock icon to reach the given state.
102
103 Args:
104 state: A value in AccountPickerScreen.SmartLockState
105 wait_time_secs: The time to wait
106 Returns:
107 True if the state is reached within the wait time, else False.
108 """
109 try:
110 util.WaitFor(lambda: self.smart_lock_state == state, wait_time_secs)
111 return True
112 except exceptions.TimeoutException:
113 return False
114
115 def EnterPassword(self):
116 """ Enters the password to unlock or sign-in.
117
118 Raises an exception if entering the password fails to enter/resume the user
119 session.
Ilya Sherman 2015/03/25 23:17:29 The Python style is to list "Raises:" just as you
Tim Song 2015/03/27 18:05:26 Done.
120 """
121 assert(self.auth_type == self.AuthType.OFFLINE_PASSWORD or
122 self.auth_type == self.AuthType.FORCE_OFFLINE_PASSWORD)
123 oobe = self._oobe
124 oobe.EvaluateJavaScript(
125 '%s.passwordElement.value = "%s"' % (
126 self._GET_POD_JS, self._chromeos.password))
127 oobe.EvaluateJavaScript(
128 '%s.activate()' % self._GET_POD_JS)
129 util.WaitFor(lambda: (self._chromeos.session_state ==
130 ChromeOS.SessionState.IN_SESSION),
131 5)
132
133 def UnlockWithClick(self):
134 """ Clicks the user pod to unlock or sign-in. """
135 assert(self.auth_type == self.AuthType.USER_CLICK)
136 self._oobe.EvaluateJavaScript('%s.activate()' % self._GET_POD_JS)
137
138
139 class SmartLockSettings(object):
140 """ Wrapper for the Smart Lock settings in chromeos://settings.
141 """
142 def __init__(self, tab, chromeos):
143 """
144 Args:
145 tab: Inspector page of the chromeos://settings tag.
146 chromeos: The parent Chrome wrapper.
147 """
148 self._tab = tab
149 self._chromeos = chromeos
150
151 @property
152 def smart_lock_enabled(self):
153 ''' Returns true if the settings show that Smart Lock is enabled. '''
154 return self._tab.EvaluateJavaScript(
155 '!document.getElementById("easy-unlock-enabled").hidden')
156
157 def TurnOffSmartLock(self):
158 """ Turns off Smart Lock.
159
160 Smart Lock is turned off by clicking the turn-off button and navigating
161 through the resulting overlay.
162
163 Raises an exception if turning off Smart Lock fails.
164 """
165 assert(self.smart_lock_enabled)
166 tab = self._tab
167 tab.EvaluateJavaScript(
168 'document.getElementById("easy-unlock-turn-off-button").click()')
169 util.WaitFor(lambda: tab.EvaluateJavaScript(
170 '!document.getElementById("easy-unlock-turn-off-overlay").hidden && '
171 'document.getElementById("easy-unlock-turn-off-confirm") != null'),
172 10)
173 tab.EvaluateJavaScript(
174 'document.getElementById("easy-unlock-turn-off-confirm").click()')
175 util.WaitFor(lambda: tab.EvaluateJavaScript(
176 '!document.getElementById("easy-unlock-disabled").hidden'), 15)
177
178 def StartSetup(self):
179 """ Starts the Smart Lock setup flow by clicking the button.
180 """
181 assert(not self.smart_lock_enabled)
182 self._tab.EvaluateJavaScript(
183 'document.getElementById("easy-unlock-setup-button").click()')
184
185 def StartSetupAndReturnApp(self):
186 """ Runs the setup and returns the wrapper to the setup app.
187
188 After clicking the setup button in the settings page, enter the password to
189 reauthenticate the user before the app launches.
190
191 Raises an exception if setup fails.
192
193 Returns:
194 A SmartLockApp object of the app that was launched.
195 """
196 self.StartSetup()
197 util.WaitFor(lambda: (self._chromeos.session_state ==
198 ChromeOS.SessionState.LOCK_SCREEN),
199 5)
200 lock_screen = self._chromeos.GetAccountPickerScreen()
201 lock_screen.EnterPassword()
202 util.WaitFor(lambda: self._chromeos.GetSmartLockApp() is not None, 10)
203 return self._chromeos.GetSmartLockApp()
204
205
206 class SmartLockApp(object):
207 """ Wrapper for the Smart Lock setup dialog.
208
209 Note: This does not include the app's background page.
210 """
211
212 class PairingState:
213 """ The current state of the setup flow. """
214 SCAN = 'scan'
215 PAIR = 'pair'
216 CLICK_FOR_TRIAL_RUN = 'click_for_trial_run'
217 TRIAL_RUN_COMPLETED = 'trial_run_completed'
218
219 def __init__(self, app_page, chromeos):
220 """
221 Args:
222 app_page: Inspector page of the app window.
223 chromeos: The parent Chrome wrapper.
224 """
225 self._app_page = app_page
226 self._chromeos = chromeos
227
228 @property
229 def pairing_state(self):
230 ''' Returns the state the app is currently in.
231
232 Raises an exception if the state is unknown.
233 '''
234 state = self._app_page.EvaluateJavaScript(
235 'document.body.getAttribute("step")')
236 if state == 'scan':
237 return SmartLockApp.PairingState.SCAN
238 elif state == 'pair':
239 return SmartLockApp.PairingState.PAIR
240 elif state == 'complete':
241 button_text = self._app_page.EvaluateJavaScript(
242 'document.getElementById("pairing-button").textContent')
243 button_text = button_text.strip().lower()
244 if button_text == 'try it out':
245 return SmartLockApp.PairingState.CLICK_FOR_TRIAL_RUN
246 elif button_text == 'done':
247 return SmartLockApp.PairingState.TRIAL_RUN_COMPLETED
248 else:
249 raise ValueError('Unknown button text: %s', button_text)
250 else:
251 raise ValueError('Unknown pairing state: %s' % state)
252
253 def FindPhone(self, retries=3):
254 """ Starts the initial step to find nearby phones.
255
256 The app must be in the SCAN state.
257
258 Args:
259 retries: The number of times to retry if no phones are found.
260 Returns:
261 True if a phone is found, else False.
262 """
263 assert(self.pairing_state == self.PairingState.SCAN)
264 for _ in xrange(retries):
265 self._ClickPairingButton()
266 if self.pairing_state == self.PairingState.PAIR:
267 return True
268 # Wait a few seconds before retrying.
269 time.sleep(10)
270 return False
271
272 def PairPhone(self):
273 """ Starts the step of finding nearby phones.
274
275 The app must be in the PAIR state.
276
277 Returns:
278 True if pairing succeeded, else False.
279 """
280 assert(self.pairing_state == self.PairingState.PAIR)
281 self._ClickPairingButton()
282 return self.pairing_state == self.PairingState.CLICK_FOR_TRIAL_RUN
283
284 def StartTrialRun(self):
285 """ Starts the trial run.
286
287 The app must be in the CLICK_FOR_TRIAL_RUN state.
288 Raises an exception if starting the trial run fails.
289 """
290 assert(self.pairing_state == self.PairingState.CLICK_FOR_TRIAL_RUN)
291 self._app_page.EvaluateJavaScript(
292 'document.getElementById("pairing-button").click()')
293 util.WaitFor(lambda: (self._chromeos.session_state ==
294 ChromeOS.SessionState.LOCK_SCREEN),
295 10)
296
297 def DismissApp(self):
298 """ Dismisses the app after setup is completed.
299
300 The app must be in the TRIAL_RUN_COMPLETED state.
301 """
302 assert(self.pairing_state == self.PairingState.TRIAL_RUN_COMPLETED)
303 self._app_page.EvaluateJavaScript(
304 'document.getElementById("pairing-button").click()')
305
306 def _ClickPairingButton(self):
307 self._app_page.EvaluateJavaScript(
308 'document.getElementById("pairing-button").click()')
309 util.WaitFor(lambda: self._app_page.EvaluateJavaScript(
310 '!document.getElementById("pairing-button").disabled'), 60)
311 util.WaitFor(lambda: self._app_page.EvaluateJavaScript(
312 '!document.getElementById("pairing-button-title")'
313 '.classList.contains("animated-fade-out")'), 5)
314 util.WaitFor(lambda: self._app_page.EvaluateJavaScript(
315 '!document.getElementById("pairing-button-title")'
316 '.classList.contains("animated-fade-in")'), 5)
317
318
319 class ChromeOS(object):
320 """ Wrapper for a remote ChromeOS device.
321
322 Operations performed through this wrapper are sent through the network to
323 Chrome using the Chrome DevTools API. Therefore, any function may throw an
324 exception if the communication to the remote device is severed.
325 """
326
327 class SessionState:
328 """ The state of the user session.
329 """
330 SIGNIN_SCREEN = 'signin_screen'
331 IN_SESSION = 'in_session'
332 LOCK_SCREEN = 'lock_screen'
333
334 _SMART_LOCK_SETTINGS_URL = 'chrome://settings/search#Smart%20Lock'
335
336 def __init__(self, remote_address, username, password, ssh_port=None):
337 """
338 Args:
339 remote_address: The remote address of the cros device.
340 username: The username of the account to test.
341 password: The password of the account to test.
342 ssh_port: The ssh port to connect to.
343 """
344 self._remote_address = remote_address;
345 self._username = username
346 self._password = password
347 self._ssh_port = ssh_port
348 self._browser = None
349 self._cros_interface = None
350 self._background_page = None
351 self._processes = []
352
353 @property
354 def username(self):
355 ''' Returns the username of the user to login. '''
356 return self._username
357
358 @property
359 def password(self):
360 ''' Returns the password of the user to login. '''
361 return self._password
362
363 @property
364 def session_state(self):
365 ''' Returns the state of the user session. '''
366 assert(self._browser is not None)
367 if self._browser.oobe_exists:
368 if self._cros_interface.IsCryptohomeMounted(self.username, False):
369 return self.SessionState.LOCK_SCREEN
370 else:
371 return self.SessionState.SIGNIN_SCREEN
372 else:
373 return self.SessionState.IN_SESSION;
374
375 @property
376 def cryptauth_access_token(self):
377 try:
378 util.WaitFor(lambda: self._background_page.EvaluateJavaScript(
379 'var __token = __token || null; '
380 'chrome.identity.getAuthToken(function(token) {'
381 ' __token = token;'
382 '}); '
383 '__token != null'), 5)
384 return self._background_page.EvaluateJavaScript('__token');
385 except exceptions.TimeoutException:
386 logger.error('Failed to get access token.');
387 return ''
388
389 def __enter__(self):
390 return self
391
392 def __exit__(self, *args):
393 if self._browser is not None:
394 self._browser.Close()
395 if self._cros_interface is not None:
396 self._cros_interface.CloseConnection()
397 for process in self._processes:
398 process.terminate()
399
400 def Start(self, local_app_path=None):
401 """ Connects to the ChromeOS device and logs in.
402 Args:
403 local_app_path: A path on the local device containing the Smart Lock app
404 to use instead of the app on the ChromeOS device.
405 Return:
406 |self| for using in a "with" statement.
407 """
408 assert(self._browser is None)
409
410 finder_opts = browser_options.BrowserFinderOptions('cros-chrome')
411 finder_opts.CreateParser().parse_args(args=[])
412 finder_opts.cros_remote = self._remote_address
413 if self._ssh_port is not None:
414 finder_opts.cros_remote_ssh_port = self._ssh_port
415 finder_opts.verbosity = 1
416
417 browser_opts = finder_opts.browser_options
418 browser_opts.create_browser_with_oobe = True
419 browser_opts.disable_component_extensions_with_background_pages = False
420 browser_opts.gaia_login = True
421 browser_opts.username = self._username
422 browser_opts.password = self._password
423 browser_opts.auto_login = True
424
425 self._cros_interface = cros_interface.CrOSInterface(
426 finder_opts.cros_remote,
427 finder_opts.cros_remote_ssh_port,
428 finder_opts.cros_ssh_identity)
429
430 browser_opts.disable_default_apps = local_app_path is not None
431 if local_app_path is not None:
432 easy_unlock_app = extension_to_load.ExtensionToLoad(
433 path=local_app_path,
434 browser_type='cros-chrome',
435 is_component=True)
436 finder_opts.extensions_to_load.append(easy_unlock_app)
437
438 retries = 3
439 while self._browser is not None or retries > 0:
440 try:
441 browser_to_create = browser_finder.FindBrowser(finder_opts)
442 self._browser = browser_to_create.Create(finder_opts);
443 break;
444 except (exceptions.LoginException) as e:
445 logger.error('Timed out logging in: %s' % e);
446 if retries == 1:
447 raise
448
449 bg_page_path = '/_generated_background_page.html'
450 util.WaitFor(
451 lambda: self._FindSmartLockAppPage(bg_page_path) is not None,
452 10);
453 self._background_page = self._FindSmartLockAppPage(bg_page_path)
454 return self
455
456 def GetAccountPickerScreen(self):
457 """ Returns the wrapper for the lock screen or sign-in screen.
458
459 Return:
460 An instance of AccountPickerScreen.
461 An exception will be raised if the wrapper can't be created.
462 """
463 assert(self._browser is not None)
464 assert(self.session_state == self.SessionState.LOCK_SCREEN or
465 self.session_state == self.SessionState.SIGNIN_SCREEN)
466 oobe = self._browser.oobe
467 def IsLockScreenResponsive():
468 return (oobe.EvaluateJavaScript("typeof Oobe == 'function'") and
469 oobe.EvaluateJavaScript(
470 "typeof Oobe.authenticateForTesting == 'function'"))
471 util.WaitFor(IsLockScreenResponsive, 10)
472 util.WaitFor(lambda: oobe.EvaluateJavaScript(
473 'document.getElementById("pod-row") && '
474 'document.getElementById("pod-row").pods && '
475 'document.getElementById("pod-row").pods.length > 0'), 10)
476 return AccountPickerScreen(oobe, self)
477
478 def GetSmartLockSettings(self):
479 """ Returns the wrapper for the Smart Lock settings.
480 A tab will be navigated to chrome://settings if it does not exist.
481
482 Return:
483 An instance of SmartLockSettings.
484 An exception will be raised if the wrapper can't be created.
485 """
486 if not len(self._browser.tabs):
487 self._browser.New()
488 tab = self._browser.tabs[0]
489 url = tab.EvaluateJavaScript('document.location.href')
490 if url != self._SMART_LOCK_SETTINGS_URL:
491 tab.Navigate(self._SMART_LOCK_SETTINGS_URL)
492
493 # Wait for settings page to be responsive.
494 util.WaitFor(lambda: tab.EvaluateJavaScript(
495 'document.getElementById("easy-unlock-disabled") && '
496 'document.getElementById("easy-unlock-enabled") && '
497 '(!document.getElementById("easy-unlock-disabled").hidden || '
498 ' !document.getElementById("easy-unlock-enabled").hidden)'), 10)
499 settings = SmartLockSettings(tab, self)
500 logger.info('Started Smart Lock settings: enabled=%s' %
501 settings.smart_lock_enabled)
502 return settings
503
504 def GetSmartLockApp(self):
505 """ Returns the wrapper for the Smart Lock setup app.
506
507 Return:
508 An instance of SmartLockApp or None if the app window does not exist.
509 """
510 app_page = self._FindSmartLockAppPage('/pairing.html')
511 if app_page is not None:
512 # Wait for app window to be responsive.
513 util.WaitFor(lambda: app_page.EvaluateJavaScript(
514 'document.getElementById("pairing-button") != null'), 10)
515 return SmartLockApp(app_page, self)
516 return None
517
518 def RunBtmon(self):
519 """ Runs the btmon command.
520 Return:
521 A subprocess.Popen object of the btmon process.
522 """
523 assert(self._cros_interface)
524 cmd = self._cros_interface.FormSSHCommandLine(['btmon'])
525 process = subprocess.Popen(args=cmd, stdout=subprocess.PIPE,
526 stderr=subprocess.PIPE)
527 self._processes.append(process)
528 return process
529
530 def _FindSmartLockAppPage(self, page_name):
531 try:
532 extensions = self._browser.extensions.GetByExtensionId(
533 'mkaemigholebcgchlkbankmihknojeak')
534 except KeyError:
535 return None
536 for extension_page in extensions:
537 pathname = extension_page.EvaluateJavaScript('document.location.pathname')
538 if pathname == page_name:
539 return extension_page
540 return None
OLDNEW
« no previous file with comments | « no previous file | components/proximity_auth/e2e_test/cryptauth.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698