1from __future__ import absolute_import, print_function
2import os
3import time
4
5from marionette_harness import MarionetteTestCase
6from marionette_driver.errors import NoAlertPresentException
7
8
9# Holds info about things we need to cleanup after the tests are done.
10class PendingCleanup:
11    desktop_backup_path = None
12    reset_profile_path = None
13    reset_profile_local_path = None
14
15    def __init__(self, profile_name_to_remove):
16        self.profile_name_to_remove = profile_name_to_remove
17
18
19class TestFirefoxRefresh(MarionetteTestCase):
20    _sandbox = "firefox-refresh"
21
22    _username = "marionette-test-login"
23    _password = "marionette-test-password"
24    _bookmarkURL = "about:mozilla"
25    _bookmarkText = "Some bookmark from Marionette"
26
27    _cookieHost = "firefox-refresh.marionette-test.mozilla.org"
28    _cookiePath = "some/cookie/path"
29    _cookieName = "somecookie"
30    _cookieValue = "some cookie value"
31
32    _historyURL = "http://firefox-refresh.marionette-test.mozilla.org/"
33    _historyTitle = "Test visit for Firefox Reset"
34
35    _formHistoryFieldName = "some-very-unique-marionette-only-firefox-reset-field"
36    _formHistoryValue = "special-pumpkin-value"
37
38    _formAutofillAvailable = False
39    _formAutofillAddressGuid = None
40
41    _expectedURLs = ["about:robots", "about:mozilla"]
42
43    def savePassword(self):
44        self.runCode(
45            """
46          let myLogin = new global.LoginInfo(
47            "test.marionette.mozilla.com",
48            "http://test.marionette.mozilla.com/some/form/",
49            null,
50            arguments[0],
51            arguments[1],
52            "username",
53            "password"
54          );
55          Services.logins.addLogin(myLogin)
56        """,
57            script_args=(self._username, self._password),
58        )
59
60    def createBookmarkInMenu(self):
61        error = self.runAsyncCode(
62            """
63          // let url = arguments[0];
64          // let title = arguments[1];
65          // let resolve = arguments[arguments.length - 1];
66          let [url, title, resolve] = arguments;
67          PlacesUtils.bookmarks.insert({
68            parentGuid: PlacesUtils.bookmarks.menuGuid, url, title
69          }).then(() => resolve(false), resolve);
70        """,
71            script_args=(self._bookmarkURL, self._bookmarkText),
72        )
73        if error:
74            print(error)
75
76    def createBookmarksOnToolbar(self):
77        error = self.runAsyncCode(
78            """
79          let resolve = arguments[arguments.length - 1];
80          let children = [];
81          for (let i = 1; i <= 5; i++) {
82            children.push({url: `about:rights?p=${i}`, title: `Bookmark ${i}`});
83          }
84          PlacesUtils.bookmarks.insertTree({
85            guid: PlacesUtils.bookmarks.toolbarGuid,
86            children
87          }).then(() => resolve(false), resolve);
88        """
89        )
90        if error:
91            print(error)
92
93    def createHistory(self):
94        error = self.runAsyncCode(
95            """
96          let resolve = arguments[arguments.length - 1];
97          PlacesUtils.history.insert({
98            url: arguments[0],
99            title: arguments[1],
100            visits: [{
101              date: new Date(Date.now() - 5000),
102              referrer: "about:mozilla"
103            }]
104          }).then(() => resolve(false),
105                  ex => resolve("Unexpected error in adding visit: " + ex));
106        """,
107            script_args=(self._historyURL, self._historyTitle),
108        )
109        if error:
110            print(error)
111
112    def createFormHistory(self):
113        error = self.runAsyncCode(
114            """
115          let updateDefinition = {
116            op: "add",
117            fieldname: arguments[0],
118            value: arguments[1],
119            firstUsed: (Date.now() - 5000) * 1000,
120          };
121          let finished = false;
122          let resolve = arguments[arguments.length - 1];
123          global.FormHistory.update(updateDefinition, {
124            handleError(error) {
125              finished = true;
126              resolve(error);
127            },
128            handleCompletion() {
129              if (!finished) {
130                resolve(false);
131              }
132            }
133          });
134        """,
135            script_args=(self._formHistoryFieldName, self._formHistoryValue),
136        )
137        if error:
138            print(error)
139
140    def createFormAutofill(self):
141        if not self._formAutofillAvailable:
142            return
143        self._formAutofillAddressGuid = self.runAsyncCode(
144            """
145          let resolve = arguments[arguments.length - 1];
146          const TEST_ADDRESS_1 = {
147            "given-name": "John",
148            "additional-name": "R.",
149            "family-name": "Smith",
150            organization: "World Wide Web Consortium",
151            "street-address": "32 Vassar Street\\\nMIT Room 32-G524",
152            "address-level2": "Cambridge",
153            "address-level1": "MA",
154            "postal-code": "02139",
155            country: "US",
156            tel: "+15195555555",
157            email: "user@example.com",
158          };
159          return global.formAutofillStorage.initialize().then(() => {
160            return global.formAutofillStorage.addresses.add(TEST_ADDRESS_1);
161          }).then(resolve);
162        """
163        )
164
165    def createCookie(self):
166        self.runCode(
167            """
168          // Expire in 15 minutes:
169          let expireTime = Math.floor(Date.now() / 1000) + 15 * 60;
170          Services.cookies.add(arguments[0], arguments[1], arguments[2], arguments[3],
171                               true, false, false, expireTime, {},
172                               Ci.nsICookie.SAMESITE_NONE, Ci.nsICookie.SCHEME_UNSET);
173        """,
174            script_args=(
175                self._cookieHost,
176                self._cookiePath,
177                self._cookieName,
178                self._cookieValue,
179            ),
180        )
181
182    def createSession(self):
183        self.runAsyncCode(
184            """
185          let resolve = arguments[arguments.length - 1];
186          const COMPLETE_STATE = Ci.nsIWebProgressListener.STATE_STOP +
187                                 Ci.nsIWebProgressListener.STATE_IS_NETWORK;
188          let {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
189          let expectedURLs = Array.from(arguments[0])
190          gBrowser.addTabsProgressListener({
191            onStateChange(browser, webprogress, request, flags, status) {
192              try {
193                request && request.QueryInterface(Ci.nsIChannel);
194              } catch (ex) {}
195              let uriLoaded = request.originalURI && request.originalURI.spec;
196              if ((flags & COMPLETE_STATE == COMPLETE_STATE) && uriLoaded &&
197                  expectedURLs.includes(uriLoaded)) {
198                TabStateFlusher.flush(browser).then(function() {
199                  expectedURLs.splice(expectedURLs.indexOf(uriLoaded), 1);
200                  if (!expectedURLs.length) {
201                    gBrowser.removeTabsProgressListener(this);
202                    resolve();
203                  }
204                });
205              }
206            }
207          });
208          let expectedTabs = new Set();
209          for (let url of expectedURLs) {
210            expectedTabs.add(gBrowser.addTab(url, {
211              triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
212            }));
213          }
214          // Close any other tabs that might be open:
215          let allTabs = Array.from(gBrowser.tabs);
216          for (let tab of allTabs) {
217            if (!expectedTabs.has(tab)) {
218              gBrowser.removeTab(tab);
219            }
220          }
221        """,  # NOQA: E501
222            script_args=(self._expectedURLs,),
223        )
224
225    def createFxa(self):
226        # This script will write an entry to the login manager and create
227        # a signedInUser.json in the profile dir.
228        self.runAsyncCode(
229            """
230          let resolve = arguments[arguments.length - 1];
231          Cu.import("resource://gre/modules/FxAccountsStorage.jsm");
232          let storage = new FxAccountsStorageManager();
233          let data = {email: "test@test.com", uid: "uid", keyFetchToken: "top-secret"};
234          storage.initialize(data);
235          storage.finalize().then(resolve);
236        """
237        )
238
239    def createSync(self):
240        # This script will write the canonical preference which indicates a user
241        # is signed into sync.
242        self.marionette.execute_script(
243            """
244            Services.prefs.setStringPref("services.sync.username", "test@test.com");
245        """
246        )
247
248    def checkPassword(self):
249        loginInfo = self.marionette.execute_script(
250            """
251          let ary = Services.logins.findLogins(
252            "test.marionette.mozilla.com",
253            "http://test.marionette.mozilla.com/some/form/",
254            null, {});
255          return ary.length ? ary : {username: "null", password: "null"};
256        """
257        )
258        self.assertEqual(len(loginInfo), 1)
259        self.assertEqual(loginInfo[0]["username"], self._username)
260        self.assertEqual(loginInfo[0]["password"], self._password)
261
262        loginCount = self.marionette.execute_script(
263            """
264          return Services.logins.getAllLogins().length;
265        """
266        )
267        # Note that we expect 2 logins - one from us, one from sync.
268        self.assertEqual(loginCount, 2, "No other logins are present")
269
270    def checkBookmarkInMenu(self):
271        titleInBookmarks = self.runAsyncCode(
272            """
273          let [url, resolve] = arguments;
274          PlacesUtils.bookmarks.fetch({url}).then(
275            bookmark => resolve(bookmark ? bookmark.title : ""),
276            ex => resolve(ex)
277          );
278        """,
279            script_args=(self._bookmarkURL,),
280        )
281        self.assertEqual(titleInBookmarks, self._bookmarkText)
282
283    def checkBookmarkToolbarVisibility(self):
284        toolbarVisible = self.marionette.execute_script(
285            """
286          const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL;
287          return Services.xulStore.getValue(BROWSER_DOCURL, "PersonalToolbar", "collapsed");
288        """
289        )
290        if toolbarVisible == "":
291            toolbarVisible = "false"
292        self.assertEqual(toolbarVisible, "false")
293
294    def checkHistory(self):
295        historyResult = self.runAsyncCode(
296            """
297          let resolve = arguments[arguments.length - 1];
298          PlacesUtils.history.fetch(arguments[0]).then(pageInfo => {
299            if (!pageInfo) {
300              resolve("No visits found");
301            } else {
302              resolve(pageInfo);
303            }
304          }).catch(e => {
305            resolve("Unexpected error in fetching page: " + e);
306          });
307        """,
308            script_args=(self._historyURL,),
309        )
310        if type(historyResult) == str:
311            self.fail(historyResult)
312            return
313
314        self.assertEqual(historyResult["title"], self._historyTitle)
315
316    def checkFormHistory(self):
317        formFieldResults = self.runAsyncCode(
318            """
319          let resolve = arguments[arguments.length - 1];
320          let results = [];
321          global.FormHistory.search(["value"], {fieldname: arguments[0]}, {
322            handleError(error) {
323              results = error;
324            },
325            handleResult(result) {
326              results.push(result);
327            },
328            handleCompletion() {
329              resolve(results);
330            },
331          });
332        """,
333            script_args=(self._formHistoryFieldName,),
334        )
335        if type(formFieldResults) == str:
336            self.fail(formFieldResults)
337            return
338
339        formFieldResultCount = len(formFieldResults)
340        self.assertEqual(
341            formFieldResultCount,
342            1,
343            "Should have exactly 1 entry for this field, got %d" % formFieldResultCount,
344        )
345        if formFieldResultCount == 1:
346            self.assertEqual(formFieldResults[0]["value"], self._formHistoryValue)
347
348        formHistoryCount = self.runAsyncCode(
349            """
350          let [resolve] = arguments;
351          let count;
352          let callbacks = {
353            handleResult: rv => count = rv,
354            handleCompletion() {
355              resolve(count);
356            },
357          };
358          global.FormHistory.count({}, callbacks);
359        """
360        )
361        self.assertEqual(
362            formHistoryCount, 1, "There should be only 1 entry in the form history"
363        )
364
365    def checkFormAutofill(self):
366        if not self._formAutofillAvailable:
367            return
368
369        formAutofillResults = self.runAsyncCode(
370            """
371          let resolve = arguments[arguments.length - 1];
372          return global.formAutofillStorage.initialize().then(() => {
373            return global.formAutofillStorage.addresses.getAll()
374          }).then(resolve);
375        """,
376        )
377        if type(formAutofillResults) == str:
378            self.fail(formAutofillResults)
379            return
380
381        formAutofillAddressCount = len(formAutofillResults)
382        self.assertEqual(
383            formAutofillAddressCount,
384            1,
385            "Should have exactly 1 saved address, got %d" % formAutofillAddressCount,
386        )
387        if formAutofillAddressCount == 1:
388            self.assertEqual(
389                formAutofillResults[0]["guid"], self._formAutofillAddressGuid
390            )
391
392    def checkCookie(self):
393        cookieInfo = self.runCode(
394            """
395          try {
396            let cookies = Services.cookies.getCookiesFromHost(arguments[0], {});
397            let cookie = null;
398            for (let hostCookie of cookies) {
399              // getCookiesFromHost returns any cookie from the BASE host.
400              if (hostCookie.rawHost != arguments[0])
401                continue;
402              if (cookie != null) {
403                return "more than 1 cookie! That shouldn't happen!";
404              }
405              cookie = hostCookie;
406            }
407            return {path: cookie.path, name: cookie.name, value: cookie.value};
408          } catch (ex) {
409            return "got exception trying to fetch cookie: " + ex;
410          }
411        """,
412            script_args=(self._cookieHost,),
413        )
414        if not isinstance(cookieInfo, dict):
415            self.fail(cookieInfo)
416            return
417        self.assertEqual(cookieInfo["path"], self._cookiePath)
418        self.assertEqual(cookieInfo["value"], self._cookieValue)
419        self.assertEqual(cookieInfo["name"], self._cookieName)
420
421    def checkSession(self):
422        tabURIs = self.runCode(
423            """
424          return [... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec)
425        """
426        )
427        self.assertSequenceEqual(tabURIs, ["about:welcomeback"])
428
429        # Dismiss modal dialog if any. This is mainly to dismiss the check for
430        # default browser dialog if it shows up.
431        try:
432            alert = self.marionette.switch_to_alert()
433            alert.dismiss()
434        except NoAlertPresentException:
435            pass
436
437        tabURIs = self.runAsyncCode(
438            """
439          let resolve = arguments[arguments.length - 1]
440          let mm = gBrowser.selectedBrowser.messageManager;
441
442          let {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
443          window.addEventListener("SSWindowStateReady", function testSSPostReset() {
444            window.removeEventListener("SSWindowStateReady", testSSPostReset, false);
445            Promise.all(gBrowser.browsers.map(b => TabStateFlusher.flush(b))).then(function() {
446              resolve([... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec));
447            });
448          }, false);
449
450          let fs = function() {
451            if (content.document.readyState === "complete") {
452              content.document.getElementById("errorTryAgain").click();
453            } else {
454              content.window.addEventListener("load", function(event) {
455                content.document.getElementById("errorTryAgain").click();
456              }, { once: true });
457            }
458          };
459
460          mm.loadFrameScript("data:application/javascript,(" + fs.toString() + ")()", true);
461        """  # NOQA: E501
462        )
463        self.assertSequenceEqual(tabURIs, self._expectedURLs)
464
465    def checkFxA(self):
466        result = self.runAsyncCode(
467            """
468          Cu.import("resource://gre/modules/FxAccountsStorage.jsm");
469          let resolve = arguments[arguments.length - 1];
470          let storage = new FxAccountsStorageManager();
471          let result = {};
472          storage.initialize();
473          storage.getAccountData().then(data => {
474            result.accountData = data;
475            return storage.finalize();
476          }).then(() => {
477            resolve(result);
478          }).catch(err => {
479            resolve(err.toString());
480          });
481        """
482        )
483        if type(result) != dict:
484            self.fail(result)
485            return
486        self.assertEqual(result["accountData"]["email"], "test@test.com")
487        self.assertEqual(result["accountData"]["uid"], "uid")
488        self.assertEqual(result["accountData"]["keyFetchToken"], "top-secret")
489
490    def checkSync(self, expect_sync_user):
491        pref_value = self.marionette.execute_script(
492            """
493            return Services.prefs.getStringPref("services.sync.username", null);
494        """
495        )
496        expected_value = "test@test.com" if expect_sync_user else None
497        self.assertEqual(pref_value, expected_value)
498
499    def checkProfile(self, has_migrated=False, expect_sync_user=True):
500        self.checkPassword()
501        self.checkBookmarkInMenu()
502        self.checkHistory()
503        self.checkFormHistory()
504        self.checkFormAutofill()
505        self.checkCookie()
506        self.checkFxA()
507        self.checkSync(expect_sync_user)
508        if has_migrated:
509            self.checkBookmarkToolbarVisibility()
510            self.checkSession()
511
512    def createProfileData(self):
513        self.savePassword()
514        self.createBookmarkInMenu()
515        self.createBookmarksOnToolbar()
516        self.createHistory()
517        self.createFormHistory()
518        self.createFormAutofill()
519        self.createCookie()
520        self.createSession()
521        self.createFxa()
522        self.createSync()
523
524    def setUpScriptData(self):
525        self.marionette.set_context(self.marionette.CONTEXT_CHROME)
526        self.runCode(
527            """
528          window.global = {};
529          global.LoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init");
530          global.profSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(Ci.nsIToolkitProfileService);
531          global.Preferences = Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
532          global.FormHistory = Cu.import("resource://gre/modules/FormHistory.jsm", {}).FormHistory;
533        """  # NOQA: E501
534        )
535        self._formAutofillAvailable = self.runCode(
536            """
537          try {
538            global.formAutofillStorage = Cu.import("resource://formautofill/FormAutofillStorage.jsm", {}).formAutofillStorage;
539          } catch(e) {
540            return false;
541          }
542          return true;
543        """  # NOQA: E501
544        )
545
546    def runCode(self, script, *args, **kwargs):
547        return self.marionette.execute_script(
548            script, new_sandbox=False, sandbox=self._sandbox, *args, **kwargs
549        )
550
551    def runAsyncCode(self, script, *args, **kwargs):
552        return self.marionette.execute_async_script(
553            script, new_sandbox=False, sandbox=self._sandbox, *args, **kwargs
554        )
555
556    def setUp(self):
557        MarionetteTestCase.setUp(self)
558        self.setUpScriptData()
559
560        self.cleanups = []
561
562    def tearDown(self):
563        # Force yet another restart with a clean profile to disconnect from the
564        # profile and environment changes we've made, to leave a more or less
565        # blank slate for the next person.
566        self.marionette.restart(clean=True, in_app=False)
567        self.setUpScriptData()
568
569        # Super
570        MarionetteTestCase.tearDown(self)
571
572        # A helper to deal with removing a load of files
573        import mozfile
574
575        for cleanup in self.cleanups:
576            if cleanup.desktop_backup_path:
577                mozfile.remove(cleanup.desktop_backup_path)
578
579            if cleanup.reset_profile_path:
580                # Remove ourselves from profiles.ini
581                self.runCode(
582                    """
583                  let name = arguments[0];
584                  let profile = global.profSvc.getProfileByName(name);
585                  profile.remove(false)
586                  global.profSvc.flush();
587                """,
588                    script_args=(cleanup.profile_name_to_remove,),
589                )
590                # Remove the local profile dir if it's not the same as the profile dir:
591                different_path = (
592                    cleanup.reset_profile_local_path != cleanup.reset_profile_path
593                )
594                if cleanup.reset_profile_local_path and different_path:
595                    mozfile.remove(cleanup.reset_profile_local_path)
596
597                # And delete all the files.
598                mozfile.remove(cleanup.reset_profile_path)
599
600    def doReset(self):
601        profileName = "marionette-test-profile-" + str(int(time.time() * 1000))
602        cleanup = PendingCleanup(profileName)
603        self.runCode(
604            """
605          // Ensure the current (temporary) profile is in profiles.ini:
606          let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
607          let profileName = arguments[1];
608          let myProfile = global.profSvc.createProfile(profD, profileName);
609          global.profSvc.flush()
610
611          // Now add the reset parameters:
612          let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
613          let prefsToKeep = Array.from(Services.prefs.getChildList("marionette."));
614          prefsToKeep.push("datareporting.policy.dataSubmissionPolicyBypassNotification");
615          let prefObj = {};
616          for (let pref of prefsToKeep) {
617            prefObj[pref] = global.Preferences.get(pref);
618          }
619          env.set("MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS", JSON.stringify(prefObj));
620          env.set("MOZ_RESET_PROFILE_RESTART", "1");
621          env.set("XRE_PROFILE_PATH", arguments[0]);
622        """,
623            script_args=(
624                self.marionette.instance.profile.profile,
625                profileName,
626            ),
627        )
628
629        profileLeafName = os.path.basename(
630            os.path.normpath(self.marionette.instance.profile.profile)
631        )
632
633        # Now restart the browser to get it reset:
634        self.marionette.restart(clean=False, in_app=True)
635        self.setUpScriptData()
636
637        # Determine the new profile path (we'll need to remove it when we're done)
638        [cleanup.reset_profile_path, cleanup.reset_profile_local_path] = self.runCode(
639            """
640          let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
641          let localD = Services.dirsvc.get("ProfLD", Ci.nsIFile);
642          return [profD.path, localD.path];
643        """
644        )
645
646        # Determine the backup path
647        cleanup.desktop_backup_path = self.runCode(
648            """
649          let container;
650          try {
651            container = Services.dirsvc.get("Desk", Ci.nsIFile);
652          } catch (ex) {
653            container = Services.dirsvc.get("Home", Ci.nsIFile);
654          }
655          let bundle = Services.strings.createBundle("chrome://mozapps/locale/profile/profileSelection.properties");
656          let dirName = bundle.formatStringFromName("resetBackupDirectory", [Services.appinfo.name]);
657          container.append(dirName);
658          container.append(arguments[0]);
659          return container.path;
660        """,  # NOQA: E501
661            script_args=(profileLeafName,),
662        )
663
664        self.assertTrue(
665            os.path.isdir(cleanup.reset_profile_path),
666            "Reset profile path should be present",
667        )
668        self.assertTrue(
669            os.path.isdir(cleanup.desktop_backup_path),
670            "Backup profile path should be present",
671        )
672        self.assertIn(cleanup.profile_name_to_remove, cleanup.reset_profile_path)
673        return cleanup
674
675    def testResetEverything(self):
676        self.createProfileData()
677
678        self.checkProfile(expect_sync_user=True)
679
680        this_cleanup = self.doReset()
681        self.cleanups.append(this_cleanup)
682
683        # Now check that we're doing OK...
684        self.checkProfile(has_migrated=True, expect_sync_user=True)
685
686    def testFxANoSync(self):
687        # This test doesn't need to repeat all the non-sync tests...
688        # Setup FxA but *not* sync
689        self.createFxa()
690
691        self.checkFxA()
692        self.checkSync(False)
693
694        this_cleanup = self.doReset()
695        self.cleanups.append(this_cleanup)
696
697        self.checkFxA()
698        self.checkSync(False)
699