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 #include "chrome/browser/ui/webui/signin/login_ui_test_utils.h"
6 
7 #include "base/run_loop.h"
8 #include "base/scoped_observer.h"
9 #include "base/strings/stringprintf.h"
10 #include "base/test/bind_test_util.h"
11 #include "base/threading/thread_task_runner_handle.h"
12 #include "base/time/time.h"
13 #include "base/timer/timer.h"
14 #include "build/build_config.h"
15 #include "chrome/browser/profiles/profile.h"
16 #include "chrome/browser/signin/account_consistency_mode_manager.h"
17 #include "chrome/browser/signin/identity_manager_factory.h"
18 #include "chrome/browser/signin/signin_promo.h"
19 #include "chrome/browser/ui/browser.h"
20 #include "chrome/browser/ui/chrome_pages.h"
21 #include "chrome/browser/ui/signin_view_controller_delegate.h"
22 #include "chrome/browser/ui/tabs/tab_strip_model.h"
23 #include "chrome/browser/ui/webui/signin/login_ui_service.h"
24 #include "chrome/browser/ui/webui/signin/login_ui_service_factory.h"
25 #include "chrome/browser/ui/webui/signin/signin_utils.h"
26 #include "chrome/test/base/ui_test_utils.h"
27 #include "components/signin/public/identity_manager/identity_manager.h"
28 #include "content/public/browser/notification_service.h"
29 #include "content/public/browser/notification_types.h"
30 #include "content/public/browser/web_contents.h"
31 #include "content/public/test/browser_test_utils.h"
32 #include "content/public/test/test_navigation_observer.h"
33 
34 using content::MessageLoopRunner;
35 
36 // anonymous namespace for signin with UI helper functions.
37 namespace {
38 
39 // When Desktop Identity Consistency (Dice) is enabled, the password field is
40 // not easily accessible on the Gaia page. This script can be used to return it.
41 const char kGetPasswordFieldFromDiceSigninPage[] =
42     "(function() {"
43     "  var e = document.getElementById('password');"
44     "  if (e == null) return null;"
45     "  return e.querySelector('input[type=password]');"
46     "})()";
47 
48 // The SignInObserver observes the identity manager and blocks until a signin
49 // success or failure notification is fired.
50 class SignInObserver : public signin::IdentityManager::Observer {
51  public:
SignInObserver()52   SignInObserver() : seen_(false), running_(false), signed_in_(false) {}
53 
54   // Returns whether a GoogleSigninSucceeded event has happened.
DidSignIn()55   bool DidSignIn() {
56     return signed_in_;
57   }
58 
59   // Blocks and waits until the user signs in. Wait() does not block if a
60   // GoogleSigninSucceeded has already occurred.
Wait()61   void Wait() {
62     if (seen_)
63       return;
64 
65     base::OneShotTimer timer;
66     timer.Start(FROM_HERE, base::TimeDelta::FromSeconds(30),
67                 base::BindRepeating(&SignInObserver::OnTimeout,
68                                     base::Unretained(this)));
69     running_ = true;
70     message_loop_runner_ = new MessageLoopRunner;
71     message_loop_runner_->Run();
72     EXPECT_TRUE(seen_);
73   }
74 
OnTimeout()75   void OnTimeout() {
76     seen_ = false;
77     if (!running_)
78       return;
79     message_loop_runner_->Quit();
80     running_ = false;
81     FAIL() << "Sign in observer timed out!";
82   }
83 
OnPrimaryAccountSet(const CoreAccountInfo & primary_account_info)84   void OnPrimaryAccountSet(
85       const CoreAccountInfo& primary_account_info) override {
86     DVLOG(1) << "Google signin succeeded.";
87     signed_in_ = true;
88     QuitLoopRunner();
89   }
90 
QuitLoopRunner()91   void QuitLoopRunner() {
92     seen_ = true;
93     if (!running_)
94       return;
95     message_loop_runner_->Quit();
96     running_ = false;
97   }
98 
99  private:
100   // Bool to mark an observed event as seen prior to calling Wait(), used to
101   // prevent the observer from blocking.
102   bool seen_;
103   // True is the message loop runner is running.
104   bool running_;
105   // True if a GoogleSigninSucceeded event has been observed.
106   bool signed_in_;
107   scoped_refptr<MessageLoopRunner> message_loop_runner_;
108 };
109 
110 // Synchronously waits for the Sync confirmation to be closed.
111 class SyncConfirmationClosedObserver : public LoginUIService::Observer {
112  public:
WaitForConfirmationClosed()113   void WaitForConfirmationClosed() {
114     if (sync_confirmation_closed_)
115       return;
116 
117     base::RunLoop run_loop;
118     quit_closure_ = run_loop.QuitClosure();
119     run_loop.Run();
120   }
121 
122  private:
OnSyncConfirmationUIClosed(LoginUIService::SyncConfirmationUIClosedResult result)123   void OnSyncConfirmationUIClosed(
124       LoginUIService::SyncConfirmationUIClosedResult result) override {
125     sync_confirmation_closed_ = true;
126     if (quit_closure_)
127       std::move(quit_closure_).Run();
128   }
129 
130   bool sync_confirmation_closed_ = false;
131   base::OnceClosure quit_closure_;
132 };
133 
RunLoopFor(base::TimeDelta duration)134 void RunLoopFor(base::TimeDelta duration) {
135   base::RunLoop run_loop;
136   base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
137       FROM_HERE, run_loop.QuitClosure(), duration);
138   run_loop.Run();
139 }
140 
141 // Returns the render frame host where Gaia credentials can be filled in.
GetSigninFrame(content::WebContents * web_contents)142 content::RenderFrameHost* GetSigninFrame(content::WebContents* web_contents) {
143   // Dice displays the Gaia page directly in a tab.
144   return web_contents->GetMainFrame();
145 }
146 
147 // Waits until the condition is met, by polling.
WaitUntilCondition(const base::RepeatingCallback<bool ()> & condition,const std::string & error_message)148 void WaitUntilCondition(const base::RepeatingCallback<bool()>& condition,
149                         const std::string& error_message) {
150   for (int attempt = 0; attempt < 10; ++attempt) {
151     if (condition.Run())
152       return;
153     RunLoopFor(base::TimeDelta::FromMilliseconds(1000));
154   }
155 
156   FAIL() << error_message;
157 }
158 
159 // Evaluates a boolean script expression in the signin frame.
EvaluateBooleanScriptInSigninFrame(Browser * browser,const std::string & script)160 bool EvaluateBooleanScriptInSigninFrame(Browser* browser,
161                                         const std::string& script) {
162   content::WebContents* web_contents =
163       browser->tab_strip_model()->GetActiveWebContents();
164   bool result = false;
165   EXPECT_TRUE(content::ExecuteScriptAndExtractBool(
166       GetSigninFrame(web_contents),
167       "window.domAutomationController.send(" + script + ");", &result));
168   return result;
169 }
170 
171 // Returns whether an element with id |element_id| exists in the signin page.
ElementExistsByIdInSigninFrame(Browser * browser,const std::string & element_id)172 bool ElementExistsByIdInSigninFrame(Browser* browser,
173                                     const std::string& element_id) {
174   return EvaluateBooleanScriptInSigninFrame(
175       browser, "document.getElementById('" + element_id + "') != null");
176 }
177 
178 // Blocks until an element with an id from |element_ids| exists in the signin
179 // page.
WaitUntilAnyElementExistsInSigninFrame(Browser * browser,const std::vector<std::string> & element_ids)180 void WaitUntilAnyElementExistsInSigninFrame(
181     Browser* browser,
182     const std::vector<std::string>& element_ids) {
183   WaitUntilCondition(
184       base::BindLambdaForTesting([&browser, &element_ids]() -> bool {
185         for (const std::string& element_id : element_ids) {
186           if (ElementExistsByIdInSigninFrame(browser, element_id))
187             return true;
188         }
189         return false;
190       }),
191       "Could not find elements in the signin frame");
192 }
193 
194 enum class SyncConfirmationDialogAction { kConfirm, kCancel };
195 
196 #if !defined(OS_CHROMEOS)
GetButtonIdForSyncConfirmationDialogAction(SyncConfirmationDialogAction action)197 std::string GetButtonIdForSyncConfirmationDialogAction(
198     SyncConfirmationDialogAction action) {
199   switch (action) {
200     case SyncConfirmationDialogAction::kConfirm:
201       return "confirmButton";
202     case SyncConfirmationDialogAction::kCancel:
203       return "cancelButton";
204   }
205 }
206 
GetRadioButtonIdForSigninEmailConfirmationDialogAction(SigninEmailConfirmationDialog::Action action)207 std::string GetRadioButtonIdForSigninEmailConfirmationDialogAction(
208     SigninEmailConfirmationDialog::Action action) {
209   switch (action) {
210     case SigninEmailConfirmationDialog::CREATE_NEW_USER:
211     case SigninEmailConfirmationDialog::CLOSE:
212       return "createNewUserRadioButton";
213     case SigninEmailConfirmationDialog::START_SYNC:
214       return "startSyncRadioButton";
215   }
216 }
217 
GetButtonIdForSigninEmailConfirmationDialogAction(SigninEmailConfirmationDialog::Action action)218 std::string GetButtonIdForSigninEmailConfirmationDialogAction(
219     SigninEmailConfirmationDialog::Action action) {
220   switch (action) {
221     case SigninEmailConfirmationDialog::CREATE_NEW_USER:
222     case SigninEmailConfirmationDialog::START_SYNC:
223       return "confirmButton";
224     case SigninEmailConfirmationDialog::CLOSE:
225       return "closeButton";
226   }
227 }
228 
GetButtonSelectorForApp(const std::string & app,const std::string & button_id)229 std::string GetButtonSelectorForApp(const std::string& app,
230                                     const std::string& button_id) {
231   return base::StringPrintf(
232       "(document.querySelector('%s') == null ? null :"
233       "document.querySelector('%s').shadowRoot.querySelector('#%s'))",
234       app.c_str(), app.c_str(), button_id.c_str());
235 }
236 #endif  // !defined(OS_CHROMEOS)
237 
238 }  // namespace
239 
240 namespace login_ui_test_utils {
241 class SigninViewControllerTestUtil {
242  public:
TryDismissSyncConfirmationDialog(Browser * browser,SyncConfirmationDialogAction action)243   static bool TryDismissSyncConfirmationDialog(
244       Browser* browser,
245       SyncConfirmationDialogAction action) {
246 #if defined(OS_CHROMEOS)
247     NOTREACHED();
248     return false;
249 #else
250     SigninViewController* signin_view_controller =
251         browser->signin_view_controller();
252     DCHECK(signin_view_controller);
253     if (!signin_view_controller->ShowsModalDialog())
254       return false;
255     content::WebContents* dialog_web_contents =
256         signin_view_controller->GetModalDialogWebContentsForTesting();
257     DCHECK(dialog_web_contents);
258     std::string button_selector = GetButtonSelectorForApp(
259         "sync-confirmation-app",
260         GetButtonIdForSyncConfirmationDialogAction(action));
261     std::string message;
262     std::string find_button_js = base::StringPrintf(
263         "if (document.readyState != 'complete') {"
264         "  window.domAutomationController.send('DocumentNotReady');"
265         "} else if (%s == null) {"
266         "  window.domAutomationController.send('NotFound');"
267         "} else {"
268         "  window.domAutomationController.send('Ok');"
269         "}",
270         button_selector.c_str());
271     EXPECT_TRUE(content::ExecuteScriptAndExtractString(
272         dialog_web_contents, find_button_js, &message));
273     if (message != "Ok")
274       return false;
275 
276     // This cannot be a synchronous call, because it closes the window as a side
277     // effect, which may cause the javascript execution to never finish.
278     content::ExecuteScriptAsync(dialog_web_contents,
279                                 button_selector + ".click();");
280     return true;
281 #endif
282   }
283 
TryCompleteSigninEmailConfirmationDialog(Browser * browser,SigninEmailConfirmationDialog::Action action)284   static bool TryCompleteSigninEmailConfirmationDialog(
285       Browser* browser,
286       SigninEmailConfirmationDialog::Action action) {
287 #if defined(OS_CHROMEOS)
288     NOTREACHED();
289     return false;
290 #else
291     SigninViewController* signin_view_controller =
292         browser->signin_view_controller();
293     DCHECK(signin_view_controller);
294     if (!signin_view_controller->ShowsModalDialog())
295       return false;
296     content::WebContents* dialog_web_contents =
297         signin_view_controller->GetModalDialogWebContentsForTesting();
298     DCHECK(dialog_web_contents);
299     std::string radio_button_selector = GetButtonSelectorForApp(
300         "signin-email-confirmation-app",
301         GetRadioButtonIdForSigninEmailConfirmationDialogAction(action));
302     std::string button_selector = GetButtonSelectorForApp(
303         "signin-email-confirmation-app",
304         GetButtonIdForSigninEmailConfirmationDialogAction(action));
305     std::string message;
306     std::string find_button_js = base::StringPrintf(
307         "if (document.readyState != 'complete') {"
308         "  window.domAutomationController.send('DocumentNotReady');"
309         "} else if (%s == null || %s == null) {"
310         "  window.domAutomationController.send('NotFound');"
311         "} else {"
312         "  window.domAutomationController.send('Ok');"
313         "}",
314         radio_button_selector.c_str(), button_selector.c_str());
315     EXPECT_TRUE(content::ExecuteScriptAndExtractString(
316         dialog_web_contents, find_button_js, &message));
317     if (message != "Ok")
318       return false;
319 
320     // This cannot be a synchronous call, because it closes the window as a side
321     // effect, which may cause the javascript execution to never finish.
322     content::ExecuteScriptAsync(
323         dialog_web_contents, base::StringPrintf("%s.click(); %s.click();",
324                                                 radio_button_selector.c_str(),
325                                                 button_selector.c_str()));
326     return true;
327 #endif
328   }
329 };
330 
WaitUntilUIReady(Browser * browser)331 void WaitUntilUIReady(Browser* browser) {
332   std::string message;
333   ASSERT_TRUE(content::ExecuteScriptAndExtractString(
334       browser->tab_strip_model()->GetActiveWebContents(),
335       "if (!inline.login.getAuthExtHost())"
336       "  inline.login.initialize();"
337       "var handler = function() {"
338       "  window.domAutomationController.send('ready');"
339       "};"
340       "if (inline.login.isAuthReady())"
341       "  handler();"
342       "else"
343       "  inline.login.getAuthExtHost().addEventListener('ready', handler);",
344       &message));
345   ASSERT_EQ("ready", message);
346 }
347 
SigninInNewGaiaFlow(Browser * browser,const std::string & email,const std::string & password)348 void SigninInNewGaiaFlow(Browser* browser,
349                          const std::string& email,
350                          const std::string& password) {
351   content::WebContents* web_contents =
352       browser->tab_strip_model()->GetActiveWebContents();
353 
354   WaitUntilAnyElementExistsInSigninFrame(browser, {"identifierId"});
355   std::string js = "document.getElementById('identifierId').value = '" + email +
356                    "'; document.getElementById('identifierNext').click();";
357   ASSERT_TRUE(content::ExecuteScript(GetSigninFrame(web_contents), js));
358 
359   // Fill the password input field.
360   std::string password_script = kGetPasswordFieldFromDiceSigninPage;
361   // Wait until the password field exists.
362   WaitUntilCondition(
363       base::BindLambdaForTesting([&browser, &password_script]() -> bool {
364         return EvaluateBooleanScriptInSigninFrame(browser,
365                                                   password_script + " != null");
366       }),
367       "Could not find Dice password field");
368   js = password_script + ".value = '" + password + "';";
369   js += "document.getElementById('passwordNext').click();";
370   ASSERT_TRUE(content::ExecuteScript(GetSigninFrame(web_contents), js));
371 }
372 
SigninInOldGaiaFlow(Browser * browser,const std::string & email,const std::string & password)373 void SigninInOldGaiaFlow(Browser* browser,
374                          const std::string& email,
375                          const std::string& password) {
376   content::WebContents* web_contents =
377       browser->tab_strip_model()->GetActiveWebContents();
378 
379   WaitUntilAnyElementExistsInSigninFrame(browser, {"Email"});
380   std::string js = "document.getElementById('Email').value = '" + email + ";" +
381                    "document.getElementById('next').click();";
382   ASSERT_TRUE(content::ExecuteScript(GetSigninFrame(web_contents), js));
383 
384   WaitUntilAnyElementExistsInSigninFrame(browser, {"Passwd"});
385   js = "document.getElementById('Passwd').value = '" + password + "';" +
386        "document.getElementById('signIn').click();";
387   ASSERT_TRUE(content::ExecuteScript(GetSigninFrame(web_contents), js));
388 }
389 
ExecuteJsToSigninInSigninFrame(Browser * browser,const std::string & email,const std::string & password)390 void ExecuteJsToSigninInSigninFrame(Browser* browser,
391                                     const std::string& email,
392                                     const std::string& password) {
393   WaitUntilAnyElementExistsInSigninFrame(browser, {"identifierNext", "next"});
394   if (ElementExistsByIdInSigninFrame(browser, "identifierNext"))
395     SigninInNewGaiaFlow(browser, email, password);
396   else
397     SigninInOldGaiaFlow(browser, email, password);
398 }
399 
SignInWithUI(Browser * browser,const std::string & username,const std::string & password)400 bool SignInWithUI(Browser* browser,
401                   const std::string& username,
402                   const std::string& password) {
403 #if defined(OS_CHROMEOS)
404   NOTREACHED();
405   return false;
406 #else
407   SignInObserver signin_observer;
408   ScopedObserver<signin::IdentityManager, signin::IdentityManager::Observer>
409       scoped_signin_observer(&signin_observer);
410   scoped_signin_observer.Add(
411       IdentityManagerFactory::GetForProfile(browser->profile()));
412 
413   signin_metrics::AccessPoint access_point =
414       signin_metrics::AccessPoint::ACCESS_POINT_MENU;
415   chrome::ShowBrowserSignin(browser, access_point);
416   content::WebContents* active_contents =
417       browser->tab_strip_model()->GetActiveWebContents();
418   DCHECK(active_contents);
419   content::TestNavigationObserver observer(
420       active_contents, 1, content::MessageLoopRunner::QuitMode::DEFERRED);
421   observer.Wait();
422   DVLOG(1) << "Sign in user: " << username;
423   ExecuteJsToSigninInSigninFrame(browser, username, password);
424   signin_observer.Wait();
425   return signin_observer.DidSignIn();
426 #endif
427 }
428 
DismissSyncConfirmationDialog(Browser * browser,base::TimeDelta timeout,SyncConfirmationDialogAction action)429 bool DismissSyncConfirmationDialog(Browser* browser,
430                                    base::TimeDelta timeout,
431                                    SyncConfirmationDialogAction action) {
432   SyncConfirmationClosedObserver confirmation_closed_observer;
433   ScopedObserver<LoginUIService, LoginUIService::Observer>
434       scoped_confirmation_closed_observer(&confirmation_closed_observer);
435   scoped_confirmation_closed_observer.Add(
436       LoginUIServiceFactory::GetForProfile(browser->profile()));
437 
438   const base::Time expire_time = base::Time::Now() + timeout;
439   while (base::Time::Now() <= expire_time) {
440     if (SigninViewControllerTestUtil::TryDismissSyncConfirmationDialog(
441             browser, action)) {
442       confirmation_closed_observer.WaitForConfirmationClosed();
443       return true;
444     }
445     RunLoopFor(base::TimeDelta::FromMilliseconds(1000));
446   }
447   return false;
448 }
449 
ConfirmSyncConfirmationDialog(Browser * browser,base::TimeDelta timeout)450 bool ConfirmSyncConfirmationDialog(Browser* browser, base::TimeDelta timeout) {
451   return DismissSyncConfirmationDialog(browser, timeout,
452                                        SyncConfirmationDialogAction::kConfirm);
453 }
454 
CancelSyncConfirmationDialog(Browser * browser,base::TimeDelta timeout)455 bool CancelSyncConfirmationDialog(Browser* browser, base::TimeDelta timeout) {
456   return DismissSyncConfirmationDialog(browser, timeout,
457                                        SyncConfirmationDialogAction::kCancel);
458 }
459 
CompleteSigninEmailConfirmationDialog(Browser * browser,base::TimeDelta timeout,SigninEmailConfirmationDialog::Action action)460 bool CompleteSigninEmailConfirmationDialog(
461     Browser* browser,
462     base::TimeDelta timeout,
463     SigninEmailConfirmationDialog::Action action) {
464   const base::Time expire_time = base::Time::Now() + timeout;
465   while (base::Time::Now() <= expire_time) {
466     if (SigninViewControllerTestUtil::TryCompleteSigninEmailConfirmationDialog(
467             browser, action)) {
468       return true;
469     }
470     RunLoopFor(base::TimeDelta::FromMilliseconds(1000));
471   }
472   return false;
473 }
474 
475 }  // namespace login_ui_test_utils
476