diff options
Diffstat (limited to '')
-rw-r--r-- | browser/components/migration/tests/marionette/manifest.ini | 4 | ||||
-rw-r--r-- | browser/components/migration/tests/marionette/test_refresh_firefox.py | 690 |
2 files changed, 694 insertions, 0 deletions
diff --git a/browser/components/migration/tests/marionette/manifest.ini b/browser/components/migration/tests/marionette/manifest.ini new file mode 100644 index 0000000000..afba7ead73 --- /dev/null +++ b/browser/components/migration/tests/marionette/manifest.ini @@ -0,0 +1,4 @@ +[DEFAULT] +run-if = buildapp == 'browser' + +[test_refresh_firefox.py] diff --git a/browser/components/migration/tests/marionette/test_refresh_firefox.py b/browser/components/migration/tests/marionette/test_refresh_firefox.py new file mode 100644 index 0000000000..603534ff17 --- /dev/null +++ b/browser/components/migration/tests/marionette/test_refresh_firefox.py @@ -0,0 +1,690 @@ +import os +import time + +from marionette_driver.errors import NoAlertPresentException +from marionette_harness import MarionetteTestCase + + +# Holds info about things we need to cleanup after the tests are done. +class PendingCleanup: + desktop_backup_path = None + reset_profile_path = None + reset_profile_local_path = None + + def __init__(self, profile_name_to_remove): + self.profile_name_to_remove = profile_name_to_remove + + +class TestFirefoxRefresh(MarionetteTestCase): + _sandbox = "firefox-refresh" + + _username = "marionette-test-login" + _password = "marionette-test-password" + _bookmarkURL = "about:mozilla" + _bookmarkText = "Some bookmark from Marionette" + + _cookieHost = "firefox-refresh.marionette-test.mozilla.org" + _cookiePath = "some/cookie/path" + _cookieName = "somecookie" + _cookieValue = "some cookie value" + + _historyURL = "http://firefox-refresh.marionette-test.mozilla.org/" + _historyTitle = "Test visit for Firefox Reset" + + _formHistoryFieldName = "some-very-unique-marionette-only-firefox-reset-field" + _formHistoryValue = "special-pumpkin-value" + + _formAutofillAvailable = False + _formAutofillAddressGuid = None + + _expectedURLs = ["about:robots", "about:mozilla"] + + def savePassword(self): + self.runAsyncCode( + """ + let [username, password, resolve] = arguments; + let myLogin = new global.LoginInfo( + "test.marionette.mozilla.com", + "http://test.marionette.mozilla.com/some/form/", + null, + username, + password, + "username", + "password" + ); + Services.logins.addLoginAsync(myLogin) + .then(() => resolve(false), resolve); + """, + script_args=(self._username, self._password), + ) + + def createBookmarkInMenu(self): + error = self.runAsyncCode( + """ + // let url = arguments[0]; + // let title = arguments[1]; + // let resolve = arguments[arguments.length - 1]; + let [url, title, resolve] = arguments; + PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, url, title + }).then(() => resolve(false), resolve); + """, + script_args=(self._bookmarkURL, self._bookmarkText), + ) + if error: + print(error) + + def createBookmarksOnToolbar(self): + error = self.runAsyncCode( + """ + let resolve = arguments[arguments.length - 1]; + let children = []; + for (let i = 1; i <= 5; i++) { + children.push({url: `about:rights?p=${i}`, title: `Bookmark ${i}`}); + } + PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children + }).then(() => resolve(false), resolve); + """ + ) + if error: + print(error) + + def createHistory(self): + error = self.runAsyncCode( + """ + let resolve = arguments[arguments.length - 1]; + PlacesUtils.history.insert({ + url: arguments[0], + title: arguments[1], + visits: [{ + date: new Date(Date.now() - 5000), + referrer: "about:mozilla" + }] + }).then(() => resolve(false), + ex => resolve("Unexpected error in adding visit: " + ex)); + """, + script_args=(self._historyURL, self._historyTitle), + ) + if error: + print(error) + + def createFormHistory(self): + error = self.runAsyncCode( + """ + let updateDefinition = { + op: "add", + fieldname: arguments[0], + value: arguments[1], + firstUsed: (Date.now() - 5000) * 1000, + }; + let resolve = arguments[arguments.length - 1]; + global.FormHistory.update(updateDefinition).then(() => { + resolve(false); + }, error => { + resolve("Unexpected error in adding formhistory: " + error); + }); + """, + script_args=(self._formHistoryFieldName, self._formHistoryValue), + ) + if error: + print(error) + + def createFormAutofill(self): + if not self._formAutofillAvailable: + return + self._formAutofillAddressGuid = self.runAsyncCode( + """ + let resolve = arguments[arguments.length - 1]; + const TEST_ADDRESS_1 = { + "given-name": "John", + "additional-name": "R.", + "family-name": "Smith", + organization: "World Wide Web Consortium", + "street-address": "32 Vassar Street\\\nMIT Room 32-G524", + "address-level2": "Cambridge", + "address-level1": "MA", + "postal-code": "02139", + country: "US", + tel: "+15195555555", + email: "user@example.com", + }; + return global.formAutofillStorage.initialize().then(() => { + return global.formAutofillStorage.addresses.add(TEST_ADDRESS_1); + }).then(resolve); + """ + ) + + def createCookie(self): + self.runCode( + """ + // Expire in 15 minutes: + let expireTime = Math.floor(Date.now() / 1000) + 15 * 60; + Services.cookies.add(arguments[0], arguments[1], arguments[2], arguments[3], + true, false, false, expireTime, {}, + Ci.nsICookie.SAMESITE_NONE, Ci.nsICookie.SCHEME_UNSET); + """, + script_args=( + self._cookieHost, + self._cookiePath, + self._cookieName, + self._cookieValue, + ), + ) + + def createSession(self): + self.runAsyncCode( + """ + let resolve = arguments[arguments.length - 1]; + const COMPLETE_STATE = Ci.nsIWebProgressListener.STATE_STOP + + Ci.nsIWebProgressListener.STATE_IS_NETWORK; + let { TabStateFlusher } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/TabStateFlusher.sys.mjs" + ); + let expectedURLs = Array.from(arguments[0]) + gBrowser.addTabsProgressListener({ + onStateChange(browser, webprogress, request, flags, status) { + try { + request && request.QueryInterface(Ci.nsIChannel); + } catch (ex) {} + let uriLoaded = request.originalURI && request.originalURI.spec; + if ((flags & COMPLETE_STATE == COMPLETE_STATE) && uriLoaded && + expectedURLs.includes(uriLoaded)) { + TabStateFlusher.flush(browser).then(function() { + expectedURLs.splice(expectedURLs.indexOf(uriLoaded), 1); + if (!expectedURLs.length) { + gBrowser.removeTabsProgressListener(this); + resolve(); + } + }); + } + } + }); + let expectedTabs = new Set(); + for (let url of expectedURLs) { + expectedTabs.add(gBrowser.addTab(url, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + })); + } + // Close any other tabs that might be open: + let allTabs = Array.from(gBrowser.tabs); + for (let tab of allTabs) { + if (!expectedTabs.has(tab)) { + gBrowser.removeTab(tab); + } + } + """, # NOQA: E501 + script_args=(self._expectedURLs,), + ) + + def createFxa(self): + # This script will write an entry to the login manager and create + # a signedInUser.json in the profile dir. + self.runAsyncCode( + """ + let resolve = arguments[arguments.length - 1]; + let { FxAccountsStorageManager } = ChromeUtils.import( + "resource://gre/modules/FxAccountsStorage.jsm" + ); + let storage = new FxAccountsStorageManager(); + let data = {email: "test@test.com", uid: "uid", keyFetchToken: "top-secret"}; + storage.initialize(data); + storage.finalize().then(resolve); + """ + ) + + def createSync(self): + # This script will write the canonical preference which indicates a user + # is signed into sync. + self.marionette.execute_script( + """ + Services.prefs.setStringPref("services.sync.username", "test@test.com"); + """ + ) + + def checkPassword(self): + loginInfo = self.marionette.execute_script( + """ + let ary = Services.logins.findLogins( + "test.marionette.mozilla.com", + "http://test.marionette.mozilla.com/some/form/", + null, {}); + return ary.length ? ary : {username: "null", password: "null"}; + """ + ) + self.assertEqual(len(loginInfo), 1) + self.assertEqual(loginInfo[0]["username"], self._username) + self.assertEqual(loginInfo[0]["password"], self._password) + + loginCount = self.marionette.execute_script( + """ + return Services.logins.getAllLogins().length; + """ + ) + # Note that we expect 2 logins - one from us, one from sync. + self.assertEqual(loginCount, 2, "No other logins are present") + + def checkBookmarkInMenu(self): + titleInBookmarks = self.runAsyncCode( + """ + let [url, resolve] = arguments; + PlacesUtils.bookmarks.fetch({url}).then( + bookmark => resolve(bookmark ? bookmark.title : ""), + ex => resolve(ex) + ); + """, + script_args=(self._bookmarkURL,), + ) + self.assertEqual(titleInBookmarks, self._bookmarkText) + + def checkBookmarkToolbarVisibility(self): + toolbarVisible = self.marionette.execute_script( + """ + const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL; + return Services.xulStore.getValue(BROWSER_DOCURL, "PersonalToolbar", "collapsed"); + """ + ) + if toolbarVisible == "": + toolbarVisible = "false" + self.assertEqual(toolbarVisible, "false") + + def checkHistory(self): + historyResult = self.runAsyncCode( + """ + let resolve = arguments[arguments.length - 1]; + PlacesUtils.history.fetch(arguments[0]).then(pageInfo => { + if (!pageInfo) { + resolve("No visits found"); + } else { + resolve(pageInfo); + } + }).catch(e => { + resolve("Unexpected error in fetching page: " + e); + }); + """, + script_args=(self._historyURL,), + ) + if type(historyResult) == str: + self.fail(historyResult) + return + + self.assertEqual(historyResult["title"], self._historyTitle) + + def checkFormHistory(self): + formFieldResults = self.runAsyncCode( + """ + let resolve = arguments[arguments.length - 1]; + let results = []; + global.FormHistory.search(["value"], {fieldname: arguments[0]}) + .then(resolve); + """, + script_args=(self._formHistoryFieldName,), + ) + if type(formFieldResults) == str: + self.fail(formFieldResults) + return + + formFieldResultCount = len(formFieldResults) + self.assertEqual( + formFieldResultCount, + 1, + "Should have exactly 1 entry for this field, got %d" % formFieldResultCount, + ) + if formFieldResultCount == 1: + self.assertEqual(formFieldResults[0]["value"], self._formHistoryValue) + + formHistoryCount = self.runAsyncCode( + """ + let [resolve] = arguments; + global.FormHistory.count({}).then(resolve); + """ + ) + self.assertEqual( + formHistoryCount, 1, "There should be only 1 entry in the form history" + ) + + def checkFormAutofill(self): + if not self._formAutofillAvailable: + return + + formAutofillResults = self.runAsyncCode( + """ + let resolve = arguments[arguments.length - 1]; + return global.formAutofillStorage.initialize().then(() => { + return global.formAutofillStorage.addresses.getAll() + }).then(resolve); + """, + ) + if type(formAutofillResults) == str: + self.fail(formAutofillResults) + return + + formAutofillAddressCount = len(formAutofillResults) + self.assertEqual( + formAutofillAddressCount, + 1, + "Should have exactly 1 saved address, got %d" % formAutofillAddressCount, + ) + if formAutofillAddressCount == 1: + self.assertEqual( + formAutofillResults[0]["guid"], self._formAutofillAddressGuid + ) + + def checkCookie(self): + cookieInfo = self.runCode( + """ + try { + let cookies = Services.cookies.getCookiesFromHost(arguments[0], {}); + let cookie = null; + for (let hostCookie of cookies) { + // getCookiesFromHost returns any cookie from the BASE host. + if (hostCookie.rawHost != arguments[0]) + continue; + if (cookie != null) { + return "more than 1 cookie! That shouldn't happen!"; + } + cookie = hostCookie; + } + return {path: cookie.path, name: cookie.name, value: cookie.value}; + } catch (ex) { + return "got exception trying to fetch cookie: " + ex; + } + """, + script_args=(self._cookieHost,), + ) + if not isinstance(cookieInfo, dict): + self.fail(cookieInfo) + return + self.assertEqual(cookieInfo["path"], self._cookiePath) + self.assertEqual(cookieInfo["value"], self._cookieValue) + self.assertEqual(cookieInfo["name"], self._cookieName) + + def checkSession(self): + tabURIs = self.runCode( + """ + return [... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec) + """ + ) + self.assertSequenceEqual(tabURIs, ["about:welcomeback"]) + + # Dismiss modal dialog if any. This is mainly to dismiss the check for + # default browser dialog if it shows up. + try: + alert = self.marionette.switch_to_alert() + alert.dismiss() + except NoAlertPresentException: + pass + + tabURIs = self.runAsyncCode( + """ + let resolve = arguments[arguments.length - 1] + let mm = gBrowser.selectedBrowser.messageManager; + + window.addEventListener("SSWindowStateReady", function() { + window.addEventListener("SSTabRestored", function() { + resolve(Array.from(gBrowser.browsers, b => b.currentURI?.spec)); + }, { capture: false, once: true }); + }, { capture: false, once: true }); + + let fs = function() { + if (content.document.readyState === "complete") { + content.document.getElementById("errorTryAgain").click(); + } else { + content.window.addEventListener("load", function(event) { + content.document.getElementById("errorTryAgain").click(); + }, { once: true }); + } + }; + + Services.prefs.setBoolPref("security.allow_parent_unrestricted_js_loads", true); + mm.loadFrameScript("data:application/javascript,(" + fs.toString() + ")()", true); + Services.prefs.setBoolPref("security.allow_parent_unrestricted_js_loads", false); + """ # NOQA: E501 + ) + self.assertSequenceEqual(tabURIs, self._expectedURLs) + + def checkFxA(self): + result = self.runAsyncCode( + """ + let { FxAccountsStorageManager } = ChromeUtils.import( + "resource://gre/modules/FxAccountsStorage.jsm" + ); + let resolve = arguments[arguments.length - 1]; + let storage = new FxAccountsStorageManager(); + let result = {}; + storage.initialize(); + storage.getAccountData().then(data => { + result.accountData = data; + return storage.finalize(); + }).then(() => { + resolve(result); + }).catch(err => { + resolve(err.toString()); + }); + """ + ) + if type(result) != dict: + self.fail(result) + return + self.assertEqual(result["accountData"]["email"], "test@test.com") + self.assertEqual(result["accountData"]["uid"], "uid") + self.assertEqual(result["accountData"]["keyFetchToken"], "top-secret") + + def checkSync(self, expect_sync_user): + pref_value = self.marionette.execute_script( + """ + return Services.prefs.getStringPref("services.sync.username", null); + """ + ) + expected_value = "test@test.com" if expect_sync_user else None + self.assertEqual(pref_value, expected_value) + + def checkProfile(self, has_migrated=False, expect_sync_user=True): + self.checkPassword() + self.checkBookmarkInMenu() + self.checkHistory() + self.checkFormHistory() + self.checkFormAutofill() + self.checkCookie() + self.checkFxA() + self.checkSync(expect_sync_user) + if has_migrated: + self.checkBookmarkToolbarVisibility() + self.checkSession() + + def createProfileData(self): + self.savePassword() + self.createBookmarkInMenu() + self.createBookmarksOnToolbar() + self.createHistory() + self.createFormHistory() + self.createFormAutofill() + self.createCookie() + self.createSession() + self.createFxa() + self.createSync() + + def setUpScriptData(self): + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + self.runCode( + """ + window.global = {}; + global.LoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init"); + global.profSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(Ci.nsIToolkitProfileService); + global.Preferences = ChromeUtils.importESModule( + "resource://gre/modules/Preferences.sys.mjs" + ).Preferences; + global.FormHistory = ChromeUtils.import( + "resource://gre/modules/FormHistory.jsm" + ).FormHistory; + """ # NOQA: E501 + ) + self._formAutofillAvailable = self.runCode( + """ + try { + global.formAutofillStorage = ChromeUtils.import( + "resource://formautofill/FormAutofillStorage.jsm" + ).formAutofillStorage; + } catch(e) { + return false; + } + return true; + """ # NOQA: E501 + ) + + def runCode(self, script, *args, **kwargs): + return self.marionette.execute_script( + script, new_sandbox=False, sandbox=self._sandbox, *args, **kwargs + ) + + def runAsyncCode(self, script, *args, **kwargs): + return self.marionette.execute_async_script( + script, new_sandbox=False, sandbox=self._sandbox, *args, **kwargs + ) + + def setUp(self): + MarionetteTestCase.setUp(self) + self.setUpScriptData() + + self.cleanups = [] + + def tearDown(self): + # Force yet another restart with a clean profile to disconnect from the + # profile and environment changes we've made, to leave a more or less + # blank slate for the next person. + self.marionette.restart(in_app=False, clean=True) + self.setUpScriptData() + + # Super + MarionetteTestCase.tearDown(self) + + # A helper to deal with removing a load of files + import mozfile + + for cleanup in self.cleanups: + if cleanup.desktop_backup_path: + mozfile.remove(cleanup.desktop_backup_path) + + if cleanup.reset_profile_path: + # Remove ourselves from profiles.ini + self.runCode( + """ + let name = arguments[0]; + let profile = global.profSvc.getProfileByName(name); + profile.remove(false) + global.profSvc.flush(); + """, + script_args=(cleanup.profile_name_to_remove,), + ) + # Remove the local profile dir if it's not the same as the profile dir: + different_path = ( + cleanup.reset_profile_local_path != cleanup.reset_profile_path + ) + if cleanup.reset_profile_local_path and different_path: + mozfile.remove(cleanup.reset_profile_local_path) + + # And delete all the files. + mozfile.remove(cleanup.reset_profile_path) + + def doReset(self): + profileName = "marionette-test-profile-" + str(int(time.time() * 1000)) + cleanup = PendingCleanup(profileName) + self.runCode( + """ + // Ensure the current (temporary) profile is in profiles.ini: + let profD = Services.dirsvc.get("ProfD", Ci.nsIFile); + let profileName = arguments[1]; + let myProfile = global.profSvc.createProfile(profD, profileName); + global.profSvc.flush() + + // Now add the reset parameters: + let prefsToKeep = Array.from(Services.prefs.getChildList("marionette.")); + // Add all the modified preferences set from geckoinstance.py to avoid + // non-local connections. + prefsToKeep = prefsToKeep.concat(JSON.parse( + Services.env.get("MOZ_MARIONETTE_REQUIRED_PREFS"))); + let prefObj = {}; + for (let pref of prefsToKeep) { + prefObj[pref] = global.Preferences.get(pref); + } + Services.env.set("MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS", JSON.stringify(prefObj)); + Services.env.set("MOZ_RESET_PROFILE_RESTART", "1"); + Services.env.set("XRE_PROFILE_PATH", arguments[0]); + """, + script_args=( + self.marionette.instance.profile.profile, + profileName, + ), + ) + + profileLeafName = os.path.basename( + os.path.normpath(self.marionette.instance.profile.profile) + ) + + # Now restart the browser to get it reset: + self.marionette.restart(clean=False, in_app=True) + self.setUpScriptData() + + # Determine the new profile path (we'll need to remove it when we're done) + [cleanup.reset_profile_path, cleanup.reset_profile_local_path] = self.runCode( + """ + let profD = Services.dirsvc.get("ProfD", Ci.nsIFile); + let localD = Services.dirsvc.get("ProfLD", Ci.nsIFile); + return [profD.path, localD.path]; + """ + ) + + # Determine the backup path + cleanup.desktop_backup_path = self.runCode( + """ + let container; + try { + container = Services.dirsvc.get("Desk", Ci.nsIFile); + } catch (ex) { + container = Services.dirsvc.get("Home", Ci.nsIFile); + } + let bundle = Services.strings.createBundle("chrome://mozapps/locale/profile/profileSelection.properties"); + let dirName = bundle.formatStringFromName("resetBackupDirectory", [Services.appinfo.name]); + container.append(dirName); + container.append(arguments[0]); + return container.path; + """, # NOQA: E501 + script_args=(profileLeafName,), + ) + + self.assertTrue( + os.path.isdir(cleanup.reset_profile_path), + "Reset profile path should be present", + ) + self.assertTrue( + os.path.isdir(cleanup.desktop_backup_path), + "Backup profile path should be present", + ) + self.assertIn(cleanup.profile_name_to_remove, cleanup.reset_profile_path) + return cleanup + + def testResetEverything(self): + self.createProfileData() + + self.checkProfile(expect_sync_user=True) + + this_cleanup = self.doReset() + self.cleanups.append(this_cleanup) + + # Now check that we're doing OK... + self.checkProfile(has_migrated=True, expect_sync_user=True) + + def testFxANoSync(self): + # This test doesn't need to repeat all the non-sync tests... + # Setup FxA but *not* sync + self.createFxa() + + self.checkFxA() + self.checkSync(False) + + this_cleanup = self.doReset() + self.cleanups.append(this_cleanup) + + self.checkFxA() + self.checkSync(False) |