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)