Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / components / proximity_auth / e2e_test / cros.py
blob1f3c0911448077b283ef42eb31d1912a22755e7b
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.
5 import logging
6 import os
7 import subprocess
8 import sys
9 import time
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)
18 from telemetry.internal.browser import browser_options
19 from telemetry.internal.browser import browser_finder
20 from telemetry.core import exceptions
21 from telemetry.core import util
22 from telemetry.core import cros_interface
23 from telemetry.internal.browser import extension_to_load
25 logger = logging.getLogger('proximity_auth.%s' % __name__)
27 class AccountPickerScreen(object):
28 """ Wrapper for the ChromeOS account picker screen.
30 The account picker screen is the WebContents page used for both the lock
31 screen and signin screen.
33 Note: This class assumes the account picker screen only has one user. If there
34 are multiple user pods, the first one will be used.
35 """
37 class AuthType:
38 """ The authentication type expected for a user pod. """
39 OFFLINE_PASSWORD = 0
40 ONLINE_SIGN_IN = 1
41 NUMERIC_PIN = 2
42 USER_CLICK = 3
43 EXPAND_THEN_USER_CLICK = 4
44 FORCE_OFFLINE_PASSWORD = 5
46 class SmartLockState:
47 """ The state of the Smart Lock icon on a user pod.
48 """
49 NOT_SHOWN = 'not_shown'
50 AUTHENTICATED = 'authenticated'
51 LOCKED = 'locked'
52 HARD_LOCKED = 'hardlocked'
53 TO_BE_ACTIVATED = 'to_be_activated'
54 SPINNER = 'spinner'
56 # JavaScript expression for getting the user pod on the page
57 _GET_POD_JS = 'document.getElementById("pod-row").pods[0]'
59 def __init__(self, oobe, chromeos):
60 """
61 Args:
62 oobe: Inspector page of the OOBE WebContents.
63 chromeos: The parent Chrome wrapper.
64 """
65 self._oobe = oobe
66 self._chromeos = chromeos
68 @property
69 def is_lockscreen(self):
70 return self._oobe.EvaluateJavaScript(
71 '!document.getElementById("sign-out-user-item").hidden')
73 @property
74 def auth_type(self):
75 return self._oobe.EvaluateJavaScript('%s.authType' % self._GET_POD_JS)
77 @property
78 def smart_lock_state(self):
79 icon_shown = self._oobe.EvaluateJavaScript(
80 '!%s.customIconElement.hidden' % self._GET_POD_JS)
81 if not icon_shown:
82 return self.SmartLockState.NOT_SHOWN
83 class_list_dict = self._oobe.EvaluateJavaScript(
84 '%s.customIconElement.querySelector(".custom-icon")'
85 '.classList' % self._GET_POD_JS)
86 class_list = [v for k,v in class_list_dict.items() if k != 'length']
88 if 'custom-icon-unlocked' in class_list:
89 return self.SmartLockState.AUTHENTICATED
90 if 'custom-icon-locked' in class_list:
91 return self.SmartLockState.LOCKED
92 if 'custom-icon-hardlocked' in class_list:
93 return self.SmartLockState.HARD_LOCKED
94 if 'custom-icon-locked-to-be-activated' in class_list:
95 return self.SmartLockState.TO_BE_ACTIVATED
96 if 'custom-icon-spinner' in class_list:
97 return self.SmartLockState.SPINNER
99 def WaitForSmartLockState(self, state, wait_time_secs=60):
100 """ Waits for the Smart Lock icon to reach the given state.
102 Args:
103 state: A value in AccountPickerScreen.SmartLockState
104 wait_time_secs: The time to wait
105 Returns:
106 True if the state is reached within the wait time, else False.
108 try:
109 util.WaitFor(lambda: self.smart_lock_state == state, wait_time_secs)
110 return True
111 except exceptions.TimeoutException:
112 return False
114 def EnterPassword(self):
115 """ Enters the password to unlock or sign-in.
117 Raises:
118 TimeoutException: entering the password fails to enter/resume the user
119 session.
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),
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)
139 class SmartLockSettings(object):
140 """ Wrapper for the Smart Lock settings in chromeos://settings.
142 def __init__(self, tab, chromeos):
144 Args:
145 tab: Inspector page of the chromeos://settings tag.
146 chromeos: The parent Chrome wrapper.
148 self._tab = tab
149 self._chromeos = chromeos
151 @property
152 def is_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')
157 def TurnOffSmartLock(self):
158 """ Turns off Smart Lock.
160 Smart Lock is turned off by clicking the turn-off button and navigating
161 through the resulting overlay.
163 Raises:
164 TimeoutException: Timed out waiting for Smart Lock to be turned off.
166 assert(self.is_smart_lock_enabled)
167 tab = self._tab
168 tab.EvaluateJavaScript(
169 'document.getElementById("easy-unlock-turn-off-button").click()')
170 util.WaitFor(lambda: tab.EvaluateJavaScript(
171 '!document.getElementById("easy-unlock-turn-off-overlay").hidden && '
172 'document.getElementById("easy-unlock-turn-off-confirm") != null'),
174 tab.EvaluateJavaScript(
175 'document.getElementById("easy-unlock-turn-off-confirm").click()')
176 util.WaitFor(lambda: tab.EvaluateJavaScript(
177 '!document.getElementById("easy-unlock-disabled").hidden'), 15)
179 def StartSetup(self):
180 """ Starts the Smart Lock setup flow by clicking the button.
182 assert(not self.is_smart_lock_enabled)
183 self._tab.EvaluateJavaScript(
184 'document.getElementById("easy-unlock-setup-button").click()')
186 def StartSetupAndReturnApp(self):
187 """ Runs the setup and returns the wrapper to the setup app.
189 After clicking the setup button in the settings page, enter the password to
190 reauthenticate the user before the app launches.
192 Returns:
193 A SmartLockApp object of the app that was launched.
195 Raises:
196 TimeoutException: Timed out waiting for app.
198 self.StartSetup()
199 util.WaitFor(lambda: (self._chromeos.session_state ==
200 ChromeOS.SessionState.LOCK_SCREEN),
202 lock_screen = self._chromeos.GetAccountPickerScreen()
203 lock_screen.EnterPassword()
204 util.WaitFor(lambda: self._chromeos.GetSmartLockApp() is not None, 10)
205 return self._chromeos.GetSmartLockApp()
208 class SmartLockApp(object):
209 """ Wrapper for the Smart Lock setup dialog.
211 Note: This does not include the app's background page.
214 class PairingState:
215 """ The current state of the setup flow. """
216 SCAN = 'scan'
217 PAIR = 'pair'
218 CLICK_FOR_TRIAL_RUN = 'click_for_trial_run'
219 TRIAL_RUN_COMPLETED = 'trial_run_completed'
220 PROMOTE_SMARTLOCK_FOR_ANDROID = 'promote-smart-lock-for-android'
222 def __init__(self, app_page, chromeos):
224 Args:
225 app_page: Inspector page of the app window.
226 chromeos: The parent Chrome wrapper.
228 self._app_page = app_page
229 self._chromeos = chromeos
231 @property
232 def pairing_state(self):
233 ''' Returns the state the app is currently in.
235 Raises:
236 ValueError: The current state is unknown.
238 state = self._app_page.EvaluateJavaScript(
239 'document.body.getAttribute("step")')
240 if state == 'scan':
241 return SmartLockApp.PairingState.SCAN
242 elif state == 'pair':
243 return SmartLockApp.PairingState.PAIR
244 elif state == 'promote-smart-lock-for-android':
245 return SmartLockApp.PairingState.PROMOTE_SMARTLOCK_FOR_ANDROID
246 elif state == 'complete':
247 button_text = self._app_page.EvaluateJavaScript(
248 'document.getElementById("pairing-button").textContent')
249 button_text = button_text.strip().lower()
250 if button_text == 'try it out':
251 return SmartLockApp.PairingState.CLICK_FOR_TRIAL_RUN
252 elif button_text == 'done':
253 return SmartLockApp.PairingState.TRIAL_RUN_COMPLETED
254 else:
255 raise ValueError('Unknown button text: %s', button_text)
256 else:
257 raise ValueError('Unknown pairing state: %s' % state)
259 def FindPhone(self, retries=3):
260 """ Starts the initial step to find nearby phones.
262 The app must be in the SCAN state.
264 Args:
265 retries: The number of times to retry if no phones are found.
266 Returns:
267 True if a phone is found, else False.
269 assert(self.pairing_state == self.PairingState.SCAN)
270 for _ in xrange(retries):
271 self._ClickPairingButton()
272 if self.pairing_state == self.PairingState.PAIR:
273 return True
274 # Wait a few seconds before retrying.
275 time.sleep(10)
276 return False
278 def PairPhone(self):
279 """ Starts the step of finding nearby phones.
281 The app must be in the PAIR state.
283 Returns:
284 True if pairing succeeded, else False.
286 assert(self.pairing_state == self.PairingState.PAIR)
287 self._ClickPairingButton()
288 if self.pairing_state == self.PairingState.PROMOTE_SMARTLOCK_FOR_ANDROID:
289 self._ClickPairingButton()
290 return self.pairing_state == self.PairingState.CLICK_FOR_TRIAL_RUN
292 def StartTrialRun(self):
293 """ Starts the trial run.
295 The app must be in the CLICK_FOR_TRIAL_RUN state.
297 Raises:
298 TimeoutException: Timed out starting the trial run.
300 assert(self.pairing_state == self.PairingState.CLICK_FOR_TRIAL_RUN)
301 self._app_page.EvaluateJavaScript(
302 'document.getElementById("pairing-button").click()')
303 util.WaitFor(lambda: (self._chromeos.session_state ==
304 ChromeOS.SessionState.LOCK_SCREEN),
307 def DismissApp(self):
308 """ Dismisses the app after setup is completed.
310 The app must be in the TRIAL_RUN_COMPLETED state.
312 assert(self.pairing_state == self.PairingState.TRIAL_RUN_COMPLETED)
313 self._app_page.EvaluateJavaScript(
314 'document.getElementById("pairing-button").click()')
316 def _ClickPairingButton(self):
317 # Waits are needed because the clicks occur before the button label changes.
318 time.sleep(1)
319 self._app_page.EvaluateJavaScript(
320 'document.getElementById("pairing-button").click()')
321 time.sleep(1)
322 util.WaitFor(lambda: self._app_page.EvaluateJavaScript(
323 '!document.getElementById("pairing-button").disabled'), 60)
324 time.sleep(1)
325 util.WaitFor(lambda: self._app_page.EvaluateJavaScript(
326 '!document.getElementById("pairing-button-title")'
327 '.classList.contains("animated-fade-out")'), 5)
328 util.WaitFor(lambda: self._app_page.EvaluateJavaScript(
329 '!document.getElementById("pairing-button-title")'
330 '.classList.contains("animated-fade-in")'), 5)
333 class ChromeOS(object):
334 """ Wrapper for a remote ChromeOS device.
336 Operations performed through this wrapper are sent through the network to
337 Chrome using the Chrome DevTools API. Therefore, any function may throw an
338 exception if the communication to the remote device is severed.
341 class SessionState:
342 """ The state of the user session.
344 SIGNIN_SCREEN = 'signin_screen'
345 IN_SESSION = 'in_session'
346 LOCK_SCREEN = 'lock_screen'
348 _SMART_LOCK_SETTINGS_URL = 'chrome://settings/search#Smart%20Lock'
350 def __init__(self, remote_address, username, password, ssh_port=None):
352 Args:
353 remote_address: The remote address of the cros device.
354 username: The username of the account to test.
355 password: The password of the account to test.
356 ssh_port: The ssh port to connect to.
358 self._remote_address = remote_address
359 self._username = username
360 self._password = password
361 self._ssh_port = ssh_port
362 self._browser = None
363 self._cros_interface = None
364 self._background_page = None
365 self._processes = []
367 @property
368 def username(self):
369 ''' Returns the username of the user to login. '''
370 return self._username
372 @property
373 def password(self):
374 ''' Returns the password of the user to login. '''
375 return self._password
377 @property
378 def session_state(self):
379 ''' Returns the state of the user session. '''
380 assert(self._browser is not None)
381 if self._browser.oobe_exists:
382 if self._cros_interface.IsCryptohomeMounted(self.username, False):
383 return self.SessionState.LOCK_SCREEN
384 else:
385 return self.SessionState.SIGNIN_SCREEN
386 else:
387 return self.SessionState.IN_SESSION;
389 @property
390 def cryptauth_access_token(self):
391 try:
392 util.WaitFor(lambda: self._background_page.EvaluateJavaScript(
393 'var __token = __token || null; '
394 'chrome.identity.getAuthToken(function(token) {'
395 ' __token = token;'
396 '}); '
397 '__token != null'), 5)
398 return self._background_page.EvaluateJavaScript('__token');
399 except exceptions.TimeoutException:
400 logger.error('Failed to get access token.');
401 return ''
403 def __enter__(self):
404 return self
406 def __exit__(self, *args):
407 if self._browser is not None:
408 self._browser.Close()
409 if self._cros_interface is not None:
410 self._cros_interface.CloseConnection()
411 for process in self._processes:
412 process.terminate()
414 def Start(self, local_app_path=None):
415 """ Connects to the ChromeOS device and logs in.
416 Args:
417 local_app_path: A path on the local device containing the Smart Lock app
418 to use instead of the app on the ChromeOS device.
419 Return:
420 |self| for using in a "with" statement.
422 assert(self._browser is None)
424 finder_opts = browser_options.BrowserFinderOptions('cros-chrome')
425 finder_opts.CreateParser().parse_args(args=[])
426 finder_opts.cros_remote = self._remote_address
427 if self._ssh_port is not None:
428 finder_opts.cros_remote_ssh_port = self._ssh_port
429 finder_opts.verbosity = 1
431 browser_opts = finder_opts.browser_options
432 browser_opts.create_browser_with_oobe = True
433 browser_opts.disable_component_extensions_with_background_pages = False
434 browser_opts.gaia_login = True
435 browser_opts.username = self._username
436 browser_opts.password = self._password
437 browser_opts.auto_login = True
439 self._cros_interface = cros_interface.CrOSInterface(
440 finder_opts.cros_remote,
441 finder_opts.cros_remote_ssh_port,
442 finder_opts.cros_ssh_identity)
444 browser_opts.disable_default_apps = local_app_path is not None
445 if local_app_path is not None:
446 easy_unlock_app = extension_to_load.ExtensionToLoad(
447 path=local_app_path,
448 browser_type='cros-chrome',
449 is_component=True)
450 finder_opts.extensions_to_load.append(easy_unlock_app)
452 retries = 3
453 while self._browser is not None or retries > 0:
454 try:
455 browser_to_create = browser_finder.FindBrowser(finder_opts)
456 self._browser = browser_to_create.Create(finder_opts);
457 break;
458 except (exceptions.LoginException) as e:
459 logger.error('Timed out logging in: %s' % e);
460 if retries == 1:
461 raise
463 bg_page_path = '/_generated_background_page.html'
464 util.WaitFor(
465 lambda: self._FindSmartLockAppPage(bg_page_path) is not None,
466 10);
467 self._background_page = self._FindSmartLockAppPage(bg_page_path)
468 return self
470 def GetAccountPickerScreen(self):
471 """ Returns the wrapper for the lock screen or sign-in screen.
473 Return:
474 An instance of AccountPickerScreen.
475 Raises:
476 TimeoutException: Timed out waiting for account picker screen to load.
478 assert(self._browser is not None)
479 assert(self.session_state == self.SessionState.LOCK_SCREEN or
480 self.session_state == self.SessionState.SIGNIN_SCREEN)
481 oobe = self._browser.oobe
482 def IsLockScreenResponsive():
483 return (oobe.EvaluateJavaScript("typeof Oobe == 'function'") and
484 oobe.EvaluateJavaScript(
485 "typeof Oobe.authenticateForTesting == 'function'"))
486 util.WaitFor(IsLockScreenResponsive, 10)
487 util.WaitFor(lambda: oobe.EvaluateJavaScript(
488 'document.getElementById("pod-row") && '
489 'document.getElementById("pod-row").pods && '
490 'document.getElementById("pod-row").pods.length > 0'), 10)
491 return AccountPickerScreen(oobe, self)
493 def GetSmartLockSettings(self):
494 """ Returns the wrapper for the Smart Lock settings.
495 A tab will be navigated to chrome://settings if it does not exist.
497 Return:
498 An instance of SmartLockSettings.
499 Raises:
500 TimeoutException: Timed out waiting for settings page.
502 if not len(self._browser.tabs):
503 self._browser.New()
504 tab = self._browser.tabs[0]
505 url = tab.EvaluateJavaScript('document.location.href')
506 if url != self._SMART_LOCK_SETTINGS_URL:
507 tab.Navigate(self._SMART_LOCK_SETTINGS_URL)
509 # Wait for settings page to be responsive.
510 util.WaitFor(lambda: tab.EvaluateJavaScript(
511 'document.getElementById("easy-unlock-disabled") && '
512 'document.getElementById("easy-unlock-enabled") && '
513 '(!document.getElementById("easy-unlock-disabled").hidden || '
514 ' !document.getElementById("easy-unlock-enabled").hidden)'), 10)
515 settings = SmartLockSettings(tab, self)
516 logger.info('Started Smart Lock settings: enabled=%s' %
517 settings.is_smart_lock_enabled)
518 return settings
520 def GetSmartLockApp(self):
521 """ Returns the wrapper for the Smart Lock setup app.
523 Return:
524 An instance of SmartLockApp or None if the app window does not exist.
526 app_page = self._FindSmartLockAppPage('/pairing.html')
527 if app_page is not None:
528 # Wait for app window to be responsive.
529 util.WaitFor(lambda: app_page.EvaluateJavaScript(
530 'document.getElementById("pairing-button") != null'), 10)
531 return SmartLockApp(app_page, self)
532 return None
534 def SetCryptAuthStaging(self, cryptauth_staging_url):
535 logger.info('Setting CryptAuth to Staging')
536 try:
537 self._background_page.ExecuteJavaScript(
538 'var key = app.CryptAuthClient.GOOGLE_API_URL_OVERRIDE_;'
539 'var __complete = false;'
540 'chrome.storage.local.set({key: "%s"}, function() {'
541 ' __complete = true;'
542 '});' % cryptauth_staging_url)
543 util.WaitFor(lambda: self._background_page.EvaluateJavaScript(
544 '__complete == true'), 10)
545 except exceptions.TimeoutException:
546 logger.error('Failed to override CryptAuth to staging url.')
548 def RunBtmon(self):
549 """ Runs the btmon command.
550 Return:
551 A subprocess.Popen object of the btmon process.
553 assert(self._cros_interface)
554 cmd = self._cros_interface.FormSSHCommandLine(['btmon'])
555 process = subprocess.Popen(args=cmd, stdout=subprocess.PIPE,
556 stderr=subprocess.PIPE)
557 self._processes.append(process)
558 return process
560 def _FindSmartLockAppPage(self, page_name):
561 try:
562 extensions = self._browser.extensions.GetByExtensionId(
563 'mkaemigholebcgchlkbankmihknojeak')
564 except KeyError:
565 return None
566 for extension_page in extensions:
567 pathname = extension_page.EvaluateJavaScript('document.location.pathname')
568 if pathname == page_name:
569 return extension_page
570 return None