diff options
Diffstat (limited to 'browser/components/migration/tests')
24 files changed, 3155 insertions, 0 deletions
diff --git a/browser/components/migration/tests/browser/.eslintrc.js b/browser/components/migration/tests/browser/.eslintrc.js new file mode 100644 index 000000000..3ea6eeb8c --- /dev/null +++ b/browser/components/migration/tests/browser/.eslintrc.js @@ -0,0 +1,9 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/mochitest/browser.eslintrc.js", + "../../../../../testing/mochitest/mochitest.eslintrc.js", + ] +}; + diff --git a/browser/components/migration/tests/browser/browser.ini b/browser/components/migration/tests/browser/browser.ini new file mode 100644 index 000000000..94edfe7aa --- /dev/null +++ b/browser/components/migration/tests/browser/browser.ini @@ -0,0 +1,3 @@ +[browser_undo_notification.js] +[browser_undo_notification_wording.js] +[browser_undo_notification_multiple_dismissal.js] diff --git a/browser/components/migration/tests/browser/browser_undo_notification.js b/browser/components/migration/tests/browser/browser_undo_notification.js new file mode 100644 index 000000000..6c97922e0 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_undo_notification.js @@ -0,0 +1,67 @@ +"use strict"; + +let scope = {}; +Cu.import("resource:///modules/AutoMigrate.jsm", scope); +let oldCanUndo = scope.AutoMigrate.canUndo; +let oldUndo = scope.AutoMigrate.undo; +registerCleanupFunction(function() { + scope.AutoMigrate.canUndo = oldCanUndo; + scope.AutoMigrate.undo = oldUndo; +}); + +const kExpectedNotificationId = "automigration-undo"; + +add_task(function* autoMigrationUndoNotificationShows() { + let getNotification = browser => + gBrowser.getNotificationBox(browser).getNotificationWithValue(kExpectedNotificationId); + + scope.AutoMigrate.canUndo = () => true; + let undoCalled; + scope.AutoMigrate.undo = () => { undoCalled = true }; + for (let url of ["about:newtab", "about:home"]) { + undoCalled = false; + // Can't use pushPrefEnv because of bug 1323779 + Services.prefs.setCharPref("browser.migrate.automigrate.browser", "someunknownbrowser"); + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url, false); + let browser = tab.linkedBrowser; + if (!getNotification(browser)) { + info(`Notification for ${url} not immediately present, waiting for it.`); + yield BrowserTestUtils.waitForNotificationBar(gBrowser, browser, kExpectedNotificationId); + } + + ok(true, `Got notification for ${url}`); + let notification = getNotification(browser); + let notificationBox = notification.parentNode; + notification.querySelector("button.notification-button-default").click(); + ok(!undoCalled, "Undo should not be called when clicking the default button"); + is(notification, notificationBox._closedNotification, "Notification should be closing"); + yield BrowserTestUtils.removeTab(tab); + + undoCalled = false; + Services.prefs.setCharPref("browser.migrate.automigrate.browser", "chrome"); + tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url, false); + browser = tab.linkedBrowser; + if (!getNotification(browser)) { + info(`Notification for ${url} not immediately present, waiting for it.`); + yield BrowserTestUtils.waitForNotificationBar(gBrowser, browser, kExpectedNotificationId); + } + + ok(true, `Got notification for ${url}`); + notification = getNotification(browser); + notificationBox = notification.parentNode; + // Set up the survey: + yield SpecialPowers.pushPrefEnv({set: [ + ["browser.migrate.automigrate.undo-survey", "https://example.com/?browser=%IMPORTEDBROWSER%"], + ["browser.migrate.automigrate.undo-survey-locales", "en-US"], + ]}); + let tabOpenedPromise = BrowserTestUtils.waitForNewTab(gBrowser, "https://example.com/?browser=Google%20Chrome"); + notification.querySelector("button:not(.notification-button-default)").click(); + ok(undoCalled, "Undo should be called when clicking the non-default (Don't Keep) button"); + is(notification, notificationBox._closedNotification, "Notification should be closing"); + let surveyTab = yield tabOpenedPromise; + ok(surveyTab, "Should have opened a tab with a survey"); + yield BrowserTestUtils.removeTab(surveyTab); + yield BrowserTestUtils.removeTab(tab); + } +}); + diff --git a/browser/components/migration/tests/browser/browser_undo_notification_multiple_dismissal.js b/browser/components/migration/tests/browser/browser_undo_notification_multiple_dismissal.js new file mode 100644 index 000000000..90b5d0d08 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_undo_notification_multiple_dismissal.js @@ -0,0 +1,122 @@ +"use strict"; + + +const kExpectedNotificationId = "automigration-undo"; + +/** + * Pretend we can undo something, trigger a notification, pick the undo option, + * and verify that the notifications are all dismissed immediately. + */ +add_task(function* checkNotificationsDismissed() { + yield SpecialPowers.pushPrefEnv({set: [ + ["browser.migrate.automigrate.enabled", true], + ["browser.migrate.automigrate.ui.enabled", true], + ]}); + let getNotification = browser => + gBrowser.getNotificationBox(browser).getNotificationWithValue(kExpectedNotificationId); + + Services.prefs.setCharPref("browser.migrate.automigrate.browser", "someunknownbrowser"); + + let {guid, lastModified} = yield PlacesUtils.bookmarks.insert( + {title: "Some imported bookmark", parentGuid: PlacesUtils.bookmarks.toolbarGuid, url: "http://www.example.com"} + ); + + let testUndoData = { + visits: [], + bookmarks: [{guid, lastModified: lastModified.getTime()}], + logins: [], + }; + let path = OS.Path.join(OS.Constants.Path.profileDir, "initialMigrationMetadata.jsonlz4"); + registerCleanupFunction(() => { + return OS.File.remove(path, {ignoreAbsent: true}); + }); + yield OS.File.writeAtomic(path, JSON.stringify(testUndoData), { + encoding: "utf-8", + compression: "lz4", + tmpPath: path + ".tmp", + }); + + let firstTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home", false); + if (!getNotification(firstTab.linkedBrowser)) { + info(`Notification not immediately present on first tab, waiting for it.`); + yield BrowserTestUtils.waitForNotificationBar(gBrowser, firstTab.linkedBrowser, kExpectedNotificationId); + } + let secondTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home", false); + if (!getNotification(secondTab.linkedBrowser)) { + info(`Notification not immediately present on second tab, waiting for it.`); + yield BrowserTestUtils.waitForNotificationBar(gBrowser, secondTab.linkedBrowser, kExpectedNotificationId); + } + + // Create a listener for the removal in the first tab, and a listener for bookmarks removal, + // then click 'Don't keep' in the second tab, and verify that the notification is removed + // before we start removing bookmarks. + let haveRemovedBookmark = false; + let bmObserver; + let bookmarkRemovedPromise = new Promise(resolve => { + bmObserver = { + onItemRemoved(itemId, parentId, index, itemType, uri, removedGuid) { + if (guid == removedGuid) { + haveRemovedBookmark = true; + resolve(); + } + }, + }; + PlacesUtils.bookmarks.addObserver(bmObserver, false); + registerCleanupFunction(() => PlacesUtils.bookmarks.removeObserver(bmObserver)); + }); + + let firstTabNotificationRemovedPromise = new Promise(resolve => { + let notification = getNotification(firstTab.linkedBrowser); + // Save this reference because notification.parentNode will be null once it's removed. + let notificationBox = notification.parentNode; + let mut = new MutationObserver(mutations => { + // Yucky, but we have to detect either the removal via animation (with marginTop) + // or when the element is removed. We can't just detect the element being removed + // because this happens asynchronously (after the animation) and so it'd race + // with the rest of the undo happening. + for (let mutation of mutations) { + if (mutation.target == notification && mutation.attributeName == "style" && + parseInt(notification.style.marginTop, 10) < 0) { + ok(!haveRemovedBookmark, "Should not have removed bookmark yet"); + mut.disconnect(); + resolve(); + return; + } + if (mutation.target == notificationBox && mutation.removedNodes.length && + mutation.removedNodes[0] == notification) { + ok(!haveRemovedBookmark, "Should not have removed bookmark yet"); + mut.disconnect(); + resolve(); + return; + } + } + }); + mut.observe(notification.parentNode, {childList: true}); + mut.observe(notification, {attributes: true}); + }); + + let prefResetPromise = new Promise(resolve => { + const kObservedPref = "browser.migrate.automigrate.browser"; + let obs = () => { + Services.prefs.removeObserver(kObservedPref, obs); + ok(!Services.prefs.prefHasUserValue(kObservedPref), + "Pref should have been reset"); + resolve(); + }; + Services.prefs.addObserver(kObservedPref, obs, false); + }); + + // Click "Don't keep" button: + let notificationToActivate = getNotification(secondTab.linkedBrowser); + notificationToActivate.querySelector("button:not(.notification-button-default)").click(); + info("Waiting for notification to be removed in first (background) tab"); + yield firstTabNotificationRemovedPromise; + info("Waiting for bookmark to be removed"); + yield bookmarkRemovedPromise; + info("Waiting for prefs to be reset"); + yield prefResetPromise; + + info("Removing spare tabs"); + yield BrowserTestUtils.removeTab(firstTab); + yield BrowserTestUtils.removeTab(secondTab); +}); diff --git a/browser/components/migration/tests/browser/browser_undo_notification_wording.js b/browser/components/migration/tests/browser/browser_undo_notification_wording.js new file mode 100644 index 000000000..f0a9ceec9 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_undo_notification_wording.js @@ -0,0 +1,67 @@ +"use strict"; + +let scope = {}; +Cu.import("resource:///modules/AutoMigrate.jsm", scope); +let oldCanUndo = scope.AutoMigrate.canUndo; +registerCleanupFunction(function() { + scope.AutoMigrate.canUndo = oldCanUndo; +}); + +const kExpectedNotificationId = "automigration-undo"; + +add_task(function* autoMigrationUndoNotificationShows() { + let getNotification = browser => + gBrowser.getNotificationBox(browser).getNotificationWithValue(kExpectedNotificationId); + let localizedVersionOf = str => { + if (str == "logins") { + return "passwords"; + } + if (str == "visits") { + return "history"; + } + return str; + }; + + scope.AutoMigrate.canUndo = () => true; + let url = "about:newtab"; + Services.prefs.setCharPref("browser.migrate.automigrate.browser", "someunknownbrowser"); + const kSubsets = [ + ["bookmarks", "logins", "visits"], + ["bookmarks", "logins"], + ["bookmarks", "visits"], + ["logins", "visits"], + ["bookmarks"], + ["logins"], + ["visits"], + ]; + const kAllItems = ["bookmarks", "logins", "visits"]; + for (let subset of kSubsets) { + let state = new Map(subset.map(item => [item, [{}]])); + scope.AutoMigrate._setImportedItemPrefFromState(state); + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url, false); + let browser = tab.linkedBrowser; + + if (!getNotification(browser)) { + info(`Notification for ${url} not immediately present, waiting for it.`); + yield BrowserTestUtils.waitForNotificationBar(gBrowser, browser, kExpectedNotificationId); + } + + ok(true, `Got notification for ${url}`); + let notification = getNotification(browser); + let notificationText = document.getAnonymousElementByAttribute(notification, "class", "messageText"); + notificationText = notificationText.textContent; + for (let potentiallyImported of kAllItems) { + let localizedImportItem = localizedVersionOf(potentiallyImported); + if (subset.includes(potentiallyImported)) { + ok(notificationText.includes(localizedImportItem), + "Expected notification to contain " + localizedImportItem); + } else { + ok(!notificationText.includes(localizedImportItem), + "Expected notification not to contain " + localizedImportItem); + } + } + + yield BrowserTestUtils.removeTab(tab); + } +}); + diff --git a/browser/components/migration/tests/marionette/manifest.ini b/browser/components/migration/tests/marionette/manifest.ini new file mode 100644 index 000000000..3f404e724 --- /dev/null +++ b/browser/components/migration/tests/marionette/manifest.ini @@ -0,0 +1,5 @@ +[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 000000000..b348a3dcd --- /dev/null +++ b/browser/components/migration/tests/marionette/test_refresh_firefox.py @@ -0,0 +1,416 @@ +import os +import shutil + +from marionette_harness import MarionetteTestCase + + +class TestFirefoxRefresh(MarionetteTestCase): + _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" + + _expectedURLs = ["about:robots", "about:mozilla"] + + def savePassword(self): + self.runCode(""" + let myLogin = new global.LoginInfo( + "test.marionette.mozilla.com", + "http://test.marionette.mozilla.com/some/form/", + null, + arguments[0], + arguments[1], + "username", + "password" + ); + Services.logins.addLogin(myLogin) + """, script_args=[self._username, self._password]) + + def createBookmark(self): + self.marionette.execute_script(""" + let url = arguments[0]; + let title = arguments[1]; + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarks.bookmarksMenuFolder, + makeURI(url), 0, title); + """, script_args=[self._bookmarkURL, self._bookmarkText]) + + def createHistory(self): + error = self.runAsyncCode(""" + // Copied from PlacesTestUtils, which isn't available in Marionette tests. + let didReturn; + PlacesUtils.asyncHistory.updatePlaces( + [{title: arguments[1], uri: makeURI(arguments[0]), visits: [{ + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK, + visitDate: (Date.now() - 5000) * 1000, + referrerURI: makeURI("about:mozilla"), + }] + }], + { + handleError(resultCode, place) { + didReturn = true; + marionetteScriptFinished("Unexpected error in adding visit: " + resultCode); + }, + handleResult() {}, + handleCompletion() { + if (!didReturn) { + marionetteScriptFinished(false); + } + }, + } + ); + """, 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 finished = false; + global.FormHistory.update(updateDefinition, { + handleError(error) { + finished = true; + marionetteScriptFinished(error); + }, + handleCompletion() { + if (!finished) { + marionetteScriptFinished(false); + } + } + }); + """, script_args=[self._formHistoryFieldName, self._formHistoryValue]) + if error: + print error + + 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); + """, script_args=[self._cookieHost, self._cookiePath, self._cookieName, self._cookieValue]) + + def createSession(self): + self.runAsyncCode(""" + const COMPLETE_STATE = Ci.nsIWebProgressListener.STATE_STOP + + Ci.nsIWebProgressListener.STATE_IS_NETWORK; + let {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {}); + 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); + marionetteScriptFinished(); + } + }); + } + } + }); + for (let url of expectedURLs) { + gBrowser.addTab(url); + } + """, script_args=[self._expectedURLs]) + + 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; + """) + self.assertEqual(loginCount, 1, "No other logins are present") + + def checkBookmark(self): + titleInBookmarks = self.marionette.execute_script(""" + let url = arguments[0]; + let bookmarkIds = PlacesUtils.bookmarks.getBookmarkIdsForURI(makeURI(url), {}, {}); + return bookmarkIds.length == 1 ? PlacesUtils.bookmarks.getItemTitle(bookmarkIds[0]) : ""; + """, script_args=[self._bookmarkURL]) + self.assertEqual(titleInBookmarks, self._bookmarkText) + + def checkHistory(self): + historyResults = self.runAsyncCode(""" + let placeInfos = []; + PlacesUtils.asyncHistory.getPlacesInfo(makeURI(arguments[0]), { + handleError(resultCode, place) { + placeInfos = null; + marionetteScriptFinished("Unexpected error in fetching visit: " + resultCode); + }, + handleResult(placeInfo) { + placeInfos.push(placeInfo); + }, + handleCompletion() { + if (placeInfos) { + if (!placeInfos.length) { + marionetteScriptFinished("No visits found"); + } else { + marionetteScriptFinished(placeInfos); + } + } + }, + }); + """, script_args=[self._historyURL]) + if type(historyResults) == str: + self.fail(historyResults) + return + + historyCount = len(historyResults) + self.assertEqual(historyCount, 1, "Should have exactly 1 entry for URI, got %d" % historyCount) + if historyCount == 1: + self.assertEqual(historyResults[0]['title'], self._historyTitle) + + def checkFormHistory(self): + formFieldResults = self.runAsyncCode(""" + let results = []; + global.FormHistory.search(["value"], {fieldname: arguments[0]}, { + handleError(error) { + results = error; + }, + handleResult(result) { + results.push(result); + }, + handleCompletion() { + marionetteScriptFinished(results); + }, + }); + """, 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 count; + let callbacks = { + handleResult: rv => count = rv, + handleCompletion() { + marionetteScriptFinished(count); + }, + }; + global.FormHistory.count({}, callbacks); + """) + self.assertEqual(formHistoryCount, 1, "There should be only 1 entry in the form history") + + def checkCookie(self): + cookieInfo = self.runCode(""" + try { + let cookieEnum = Services.cookies.getCookiesFromHost(arguments[0]); + let cookie = null; + while (cookieEnum.hasMoreElements()) { + let hostCookie = cookieEnum.getNext(); + hostCookie.QueryInterface(Ci.nsICookie2); + // 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"]) + + tabURIs = self.runAsyncCode(""" + let mm = gBrowser.selectedBrowser.messageManager; + let fs = function() { + content.document.getElementById("errorTryAgain").click(); + }; + let {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {}); + window.addEventListener("SSWindowStateReady", function testSSPostReset() { + window.removeEventListener("SSWindowStateReady", testSSPostReset, false); + Promise.all(gBrowser.browsers.map(b => TabStateFlusher.flush(b))).then(function() { + marionetteScriptFinished([... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec)); + }); + }, false); + mm.loadFrameScript("data:application/javascript,(" + fs.toString() + ")()", true); + """) + self.assertSequenceEqual(tabURIs, ["about:blank"] + self._expectedURLs) + pass + + def checkProfile(self, hasMigrated=False): + self.checkPassword() + self.checkBookmark() + self.checkHistory() + self.checkFormHistory() + self.checkCookie() + if hasMigrated: + self.checkSession() + + def createProfileData(self): + self.savePassword() + self.createBookmark() + self.createHistory() + self.createFormHistory() + self.createCookie() + self.createSession() + + def setUpScriptData(self): + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + self.marionette.execute_script(""" + 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 = Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences; + global.FormHistory = Cu.import("resource://gre/modules/FormHistory.jsm", {}).FormHistory; + """, new_sandbox=False, sandbox='system') + + def runCode(self, script, *args, **kwargs): + return self.marionette.execute_script(script, new_sandbox=False, sandbox='system', *args, **kwargs) + + def runAsyncCode(self, script, *args, **kwargs): + return self.marionette.execute_async_script(script, new_sandbox=False, sandbox='system', *args, **kwargs) + + def setUp(self): + MarionetteTestCase.setUp(self) + self.setUpScriptData() + + self.reset_profile_path = None + self.desktop_backup_path = None + + self.createProfileData() + + 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(clean=True, in_app=False) + self.setUpScriptData() + + # Super + MarionetteTestCase.tearDown(self) + + # Some helpers to deal with removing a load of files + import errno, stat + def handleRemoveReadonly(func, path, exc): + excvalue = exc[1] + if func in (os.rmdir, os.remove) and excvalue.errno == errno.EACCES: + os.chmod(path, stat.S_IRWXU| stat.S_IRWXG| stat.S_IRWXO) # 0777 + func(path) + else: + raise + + if self.desktop_backup_path: + shutil.rmtree(self.desktop_backup_path, ignore_errors=False, onerror=handleRemoveReadonly) + + if self.reset_profile_path: + # Remove ourselves from profiles.ini + profileLeafName = os.path.basename(os.path.normpath(self.reset_profile_path)) + self.runCode(""" + let [salt, name] = arguments[0].split("."); + let profile = global.profSvc.getProfileByName(name); + profile.remove(false) + global.profSvc.flush(); + """, script_args=[profileLeafName]) + # And delete all the files. + shutil.rmtree(self.reset_profile_path, ignore_errors=False, onerror=handleRemoveReadonly) + + def doReset(self): + self.runCode(""" + // Ensure the current (temporary) profile is in profiles.ini: + let profD = Services.dirsvc.get("ProfD", Ci.nsIFile); + let profileName = "marionette-test-profile-" + Date.now(); + let myProfile = global.profSvc.createProfile(profD, profileName); + global.profSvc.flush() + + // Now add the reset parameters: + let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment); + let allMarionettePrefs = Services.prefs.getChildList("marionette."); + let prefObj = {}; + for (let pref of allMarionettePrefs) { + let prefSuffix = pref.substr("marionette.".length); + let prefVal = global.Preferences.get(pref); + prefObj[prefSuffix] = prefVal; + } + let marionetteInfo = JSON.stringify(prefObj); + env.set("MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS", marionetteInfo); + env.set("MOZ_RESET_PROFILE_RESTART", "1"); + env.set("XRE_PROFILE_PATH", arguments[0]); + env.set("XRE_PROFILE_NAME", profileName); + """, script_args=[self.marionette.instance.profile.profile]) + + 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) + self.reset_profile_path = self.runCode(""" + let profD = Services.dirsvc.get("ProfD", Ci.nsIFile); + return profD.path; + """) + + # Determine the backup path + self.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], 1); + container.append(dirName); + container.append(arguments[0]); + return container.path; + """, script_args = [profileLeafName]) + + self.assertTrue(os.path.isdir(self.reset_profile_path), "Reset profile path should be present") + self.assertTrue(os.path.isdir(self.desktop_backup_path), "Backup profile path should be present") + + def testReset(self): + self.checkProfile() + + self.doReset() + + # Now check that we're doing OK... + self.checkProfile(hasMigrated=True) diff --git a/browser/components/migration/tests/unit/.eslintrc.js b/browser/components/migration/tests/unit/.eslintrc.js new file mode 100644 index 000000000..ba65517f9 --- /dev/null +++ b/browser/components/migration/tests/unit/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { // eslint-disable-line no-undef + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data Binary files differnew file mode 100644 index 000000000..914149c71 --- /dev/null +++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookies b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookies Binary files differnew file mode 100644 index 000000000..83d855cb3 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookies diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Local State b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Local State new file mode 100644 index 000000000..01b99455e --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Local State @@ -0,0 +1,22 @@ +{ + "profile" : { + "info_cache" : { + "Default" : { + "active_time" : 1430950755.65137, + "is_using_default_name" : true, + "is_ephemeral" : false, + "is_omitted_from_profile_list" : false, + "user_name" : "", + "background_apps" : false, + "is_using_default_avatar" : true, + "avatar_icon" : "chrome://theme/IDR_PROFILE_AVATAR_0", + "name" : "Person 1" + } + }, + "profiles_created" : 1, + "last_used" : "Default", + "last_active_profiles" : [ + "Default" + ] + } +} diff --git a/browser/components/migration/tests/unit/Library/Safari/Bookmarks.plist b/browser/components/migration/tests/unit/Library/Safari/Bookmarks.plist Binary files differnew file mode 100644 index 000000000..40783c7b1 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Bookmarks.plist diff --git a/browser/components/migration/tests/unit/head_migration.js b/browser/components/migration/tests/unit/head_migration.js new file mode 100644 index 000000000..d3c258d54 --- /dev/null +++ b/browser/components/migration/tests/unit/head_migration.js @@ -0,0 +1,69 @@ +"use strict"; + +/* exported gProfD, promiseMigration, registerFakePath */ + +var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; + +Cu.importGlobalProperties([ "URL" ]); + +Cu.import("resource:///modules/MigrationUtils.jsm"); +Cu.import("resource://gre/modules/LoginHelper.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/PlacesUtils.jsm"); +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/PromiseUtils.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://testing-common/TestUtils.jsm"); +Cu.import("resource://testing-common/PlacesTestUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); + +// Initialize profile. +var gProfD = do_get_profile(); + +Cu.import("resource://testing-common/AppInfo.jsm"); /* globals updateAppInfo */ +updateAppInfo(); + +/** + * Migrates the requested resource and waits for the migration to be complete. + */ +function promiseMigration(migrator, resourceType, aProfile = null) { + // Ensure resource migration is available. + let availableSources = migrator.getMigrateData(aProfile, false); + Assert.ok((availableSources & resourceType) > 0, "Resource supported by migrator"); + + return new Promise (resolve => { + Services.obs.addObserver(function onMigrationEnded() { + Services.obs.removeObserver(onMigrationEnded, "Migration:Ended"); + resolve(); + }, "Migration:Ended", false); + + migrator.migrate(resourceType, null, aProfile); + }); +} + +/** + * Replaces a directory service entry with a given nsIFile. + */ +function registerFakePath(key, file) { + // Register our own provider for the Library directory. + let provider = { + getFile(prop, persistent) { + persistent.value = true; + if (prop == key) { + return file; + } + throw Cr.NS_ERROR_FAILURE; + }, + QueryInterface: XPCOMUtils.generateQI([ Ci.nsIDirectoryServiceProvider ]) + }; + Services.dirsvc.QueryInterface(Ci.nsIDirectoryService) + .registerProvider(provider); + do_register_cleanup(() => { + Services.dirsvc.QueryInterface(Ci.nsIDirectoryService) + .unregisterProvider(provider); + }); +} diff --git a/browser/components/migration/tests/unit/test_Chrome_cookies.js b/browser/components/migration/tests/unit/test_Chrome_cookies.js new file mode 100644 index 000000000..006693951 --- /dev/null +++ b/browser/components/migration/tests/unit/test_Chrome_cookies.js @@ -0,0 +1,51 @@ +"use strict"; + +Cu.import("resource://gre/modules/ForgetAboutSite.jsm"); + +add_task(function* () { + registerFakePath("ULibDir", do_get_file("Library/")); + let migrator = MigrationUtils.getMigrator("chrome"); + + Assert.ok(migrator.sourceExists, "Sanity check the source exists"); + + const COOKIE = { + expiry: 2145934800, + host: "unencryptedcookie.invalid", + isHttpOnly: false, + isSession: false, + name: "testcookie", + path: "/", + value: "testvalue", + }; + + // Sanity check. + Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 0, + "There are no cookies initially"); + + const PROFILE = { + id: "Default", + name: "Person 1", + }; + + // Migrate unencrypted cookies. + yield promiseMigration(migrator, MigrationUtils.resourceTypes.COOKIES, PROFILE); + + Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 1, + "Migrated the expected number of unencrypted cookies"); + Assert.equal(Services.cookies.countCookiesFromHost("encryptedcookie.invalid"), 0, + "Migrated the expected number of encrypted cookies"); + + // Now check the cookie details. + let enumerator = Services.cookies.getCookiesFromHost(COOKIE.host, {}); + Assert.ok(enumerator.hasMoreElements(), "Cookies available"); + let foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2); + + for (let prop of Object.keys(COOKIE)) { + Assert.equal(foundCookie[prop], COOKIE[prop], "Check cookie " + prop); + } + + // Cleanup. + ForgetAboutSite.removeDataFromDomain(COOKIE.host); + Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 0, + "There are no cookies after cleanup"); +}); diff --git a/browser/components/migration/tests/unit/test_Chrome_passwords.js b/browser/components/migration/tests/unit/test_Chrome_passwords.js new file mode 100644 index 000000000..49147bd61 --- /dev/null +++ b/browser/components/migration/tests/unit/test_Chrome_passwords.js @@ -0,0 +1,219 @@ +"use strict"; + +Cu.import("resource://gre/modules/OSCrypto.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +const PROFILE = { + id: "Default", + name: "Person 1", +}; + +const TEST_LOGINS = [ + { + id: 1, // id of the row in the chrome login db + username: "username", + password: "password", + hostname: "https://c9.io", + formSubmitURL: "https://c9.io", + httpRealm: null, + usernameField: "inputEmail", + passwordField: "inputPassword", + timeCreated: 1437418416037, + timePasswordChanged: 1437418416037, + timesUsed: 1, + }, + { + id: 2, + username: "username@gmail.com", + password: "password2", + hostname: "https://accounts.google.com", + formSubmitURL: "https://accounts.google.com", + httpRealm: null, + usernameField: "Email", + passwordField: "Passwd", + timeCreated: 1437418446598, + timePasswordChanged: 1437418446598, + timesUsed: 6, + }, + { + id: 3, + username: "username", + password: "password3", + hostname: "https://www.facebook.com", + formSubmitURL: "https://www.facebook.com", + httpRealm: null, + usernameField: "email", + passwordField: "pass", + timeCreated: 1437418478851, + timePasswordChanged: 1437418478851, + timesUsed: 1, + }, + { + id: 4, + username: "user", + password: "password", + hostname: "http://httpbin.org", + formSubmitURL: null, + httpRealm: "me@kennethreitz.com", // Digest auth. + usernameField: "", + passwordField: "", + timeCreated: 1437787462368, + timePasswordChanged: 1437787462368, + timesUsed: 1, + }, + { + id: 5, + username: "buser", + password: "bpassword", + hostname: "http://httpbin.org", + formSubmitURL: null, + httpRealm: "Fake Realm", // Basic auth. + usernameField: "", + passwordField: "", + timeCreated: 1437787539233, + timePasswordChanged: 1437787539233, + timesUsed: 1, + }, +]; + +var crypto = new OSCrypto(); +var dbConn; + +function promiseSetPassword(login) { + return new Promise((resolve, reject) => { + let stmt = dbConn.createAsyncStatement(` + UPDATE logins + SET password_value = :password_value + WHERE rowid = :rowid + `); + let passwordValue = crypto.stringToArray(crypto.encryptData(login.password)); + stmt.bindBlobByName("password_value", passwordValue, passwordValue.length); + stmt.params.rowid = login.id; + + stmt.executeAsync({ + handleError(aError) { + reject("Error with the query: " + aError.message); + }, + + handleCompletion(aReason) { + if (aReason === Ci.mozIStorageStatementCallback.REASON_FINISHED) { + resolve(); + } else { + reject("Query has failed: " + aReason); + } + }, + }); + stmt.finalize(); + }); +} + +function checkLoginsAreEqual(passwordManagerLogin, chromeLogin, id) { + passwordManagerLogin.QueryInterface(Ci.nsILoginMetaInfo); + + Assert.equal(passwordManagerLogin.username, chromeLogin.username, + "The two logins ID " + id + " have the same username"); + Assert.equal(passwordManagerLogin.password, chromeLogin.password, + "The two logins ID " + id + " have the same password"); + Assert.equal(passwordManagerLogin.hostname, chromeLogin.hostname, + "The two logins ID " + id + " have the same hostname"); + Assert.equal(passwordManagerLogin.formSubmitURL, chromeLogin.formSubmitURL, + "The two logins ID " + id + " have the same formSubmitURL"); + Assert.equal(passwordManagerLogin.httpRealm, chromeLogin.httpRealm, + "The two logins ID " + id + " have the same httpRealm"); + Assert.equal(passwordManagerLogin.usernameField, chromeLogin.usernameField, + "The two logins ID " + id + " have the same usernameElement"); + Assert.equal(passwordManagerLogin.passwordField, chromeLogin.passwordField, + "The two logins ID " + id + " have the same passwordElement"); + Assert.equal(passwordManagerLogin.timeCreated, chromeLogin.timeCreated, + "The two logins ID " + id + " have the same timeCreated"); + Assert.equal(passwordManagerLogin.timePasswordChanged, chromeLogin.timePasswordChanged, + "The two logins ID " + id + " have the same timePasswordChanged"); + Assert.equal(passwordManagerLogin.timesUsed, chromeLogin.timesUsed, + "The two logins ID " + id + " have the same timesUsed"); +} + +function generateDifferentLogin(login) { + let newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + + newLogin.init(login.hostname, login.formSubmitURL, null, + login.username, login.password + 1, login.usernameField + 1, + login.passwordField + 1); + newLogin.QueryInterface(Ci.nsILoginMetaInfo); + newLogin.timeCreated = login.timeCreated + 1; + newLogin.timePasswordChanged = login.timePasswordChanged + 1; + newLogin.timesUsed = login.timesUsed + 1; + return newLogin; +} + +add_task(function* setup() { + let loginDataFile = do_get_file("AppData/Local/Google/Chrome/User Data/Default/Login Data"); + dbConn = Services.storage.openUnsharedDatabase(loginDataFile); + registerFakePath("LocalAppData", do_get_file("AppData/Local/")); + + do_register_cleanup(() => { + Services.logins.removeAllLogins(); + dbConn.asyncClose(); + crypto.finalize(); + }); +}); + +add_task(function* test_importIntoEmptyDB() { + for (let login of TEST_LOGINS) { + yield promiseSetPassword(login); + } + + let migrator = MigrationUtils.getMigrator("chrome"); + Assert.ok(migrator.sourceExists, "Sanity check the source exists"); + + let logins = Services.logins.getAllLogins({}); + Assert.equal(logins.length, 0, "There are no logins initially"); + + // Migrate the logins. + yield promiseMigration(migrator, MigrationUtils.resourceTypes.PASSWORDS, PROFILE); + + logins = Services.logins.getAllLogins({}); + Assert.equal(logins.length, TEST_LOGINS.length, "Check login count after importing the data"); + Assert.equal(logins.length, MigrationUtils._importQuantities.logins, + "Check telemetry matches the actual import."); + + for (let i = 0; i < TEST_LOGINS.length; i++) { + checkLoginsAreEqual(logins[i], TEST_LOGINS[i], i + 1); + } +}); + +// Test that existing logins for the same primary key don't get overwritten +add_task(function* test_importExistingLogins() { + let migrator = MigrationUtils.getMigrator("chrome"); + Assert.ok(migrator.sourceExists, "Sanity check the source exists"); + + Services.logins.removeAllLogins(); + let logins = Services.logins.getAllLogins({}); + Assert.equal(logins.length, 0, "There are no logins after removing all of them"); + + let newLogins = []; + + // Create 3 new logins that are different but where the key properties are still the same. + for (let i = 0; i < 3; i++) { + newLogins.push(generateDifferentLogin(TEST_LOGINS[i])); + Services.logins.addLogin(newLogins[i]); + } + + logins = Services.logins.getAllLogins({}); + Assert.equal(logins.length, newLogins.length, "Check login count after the insertion"); + + for (let i = 0; i < newLogins.length; i++) { + checkLoginsAreEqual(logins[i], newLogins[i], i + 1); + } + // Migrate the logins. + yield promiseMigration(migrator, MigrationUtils.resourceTypes.PASSWORDS, PROFILE); + + logins = Services.logins.getAllLogins({}); + Assert.equal(logins.length, TEST_LOGINS.length, + "Check there are still the same number of logins after re-importing the data"); + Assert.equal(logins.length, MigrationUtils._importQuantities.logins, + "Check telemetry matches the actual import."); + + for (let i = 0; i < newLogins.length; i++) { + checkLoginsAreEqual(logins[i], newLogins[i], i + 1); + } +}); diff --git a/browser/components/migration/tests/unit/test_Edge_availability.js b/browser/components/migration/tests/unit/test_Edge_availability.js new file mode 100644 index 000000000..dba0e27bb --- /dev/null +++ b/browser/components/migration/tests/unit/test_Edge_availability.js @@ -0,0 +1,20 @@ +"use strict"; + +const EDGE_AVAILABLE_MIGRATIONS = + MigrationUtils.resourceTypes.COOKIES | + MigrationUtils.resourceTypes.BOOKMARKS | + MigrationUtils.resourceTypes.HISTORY | + MigrationUtils.resourceTypes.PASSWORDS; + +add_task(function* () { + let migrator = MigrationUtils.getMigrator("edge"); + Cu.import("resource://gre/modules/AppConstants.jsm"); + Assert.equal(!!(migrator && migrator.sourceExists), AppConstants.isPlatformAndVersionAtLeast("win", "10"), + "Edge should be available for migration if and only if we're on Win 10+"); + if (migrator) { + let migratableData = migrator.getMigrateData(null, false); + Assert.equal(migratableData, EDGE_AVAILABLE_MIGRATIONS, + "All the data types we expect should be available"); + } +}); + diff --git a/browser/components/migration/tests/unit/test_Edge_db_migration.js b/browser/components/migration/tests/unit/test_Edge_db_migration.js new file mode 100644 index 000000000..56ff612d5 --- /dev/null +++ b/browser/components/migration/tests/unit/test_Edge_db_migration.js @@ -0,0 +1,471 @@ +"use strict"; + +Cu.import("resource://gre/modules/ctypes.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +let eseBackStage = Cu.import("resource:///modules/ESEDBReader.jsm"); +let ESE = eseBackStage.ESE; +let KERNEL = eseBackStage.KERNEL; +let gLibs = eseBackStage.gLibs; +let COLUMN_TYPES = eseBackStage.COLUMN_TYPES; +let declareESEFunction = eseBackStage.declareESEFunction; +let loadLibraries = eseBackStage.loadLibraries; + +let gESEInstanceCounter = 1; + +ESE.JET_COLUMNCREATE_W = new ctypes.StructType("JET_COLUMNCREATE_W", [ + {"cbStruct": ctypes.unsigned_long}, + {"szColumnName": ESE.JET_PCWSTR}, + {"coltyp": ESE.JET_COLTYP }, + {"cbMax": ctypes.unsigned_long }, + {"grbit": ESE.JET_GRBIT }, + {"pvDefault": ctypes.voidptr_t}, + {"cbDefault": ctypes.unsigned_long }, + {"cp": ctypes.unsigned_long }, + {"columnid": ESE.JET_COLUMNID}, + {"err": ESE.JET_ERR}, +]); + +function createColumnCreationWrapper({name, type, cbMax}) { + // We use a wrapper object because we need to be sure the JS engine won't GC + // data that we're "only" pointing to. + let wrapper = {}; + wrapper.column = new ESE.JET_COLUMNCREATE_W(); + wrapper.column.cbStruct = ESE.JET_COLUMNCREATE_W.size; + let wchar_tArray = ctypes.ArrayType(ctypes.char16_t); + wrapper.name = new wchar_tArray(name.length + 1); + wrapper.name.value = String(name); + wrapper.column.szColumnName = wrapper.name; + wrapper.column.coltyp = type; + let fallback = 0; + switch (type) { + case COLUMN_TYPES.JET_coltypText: + fallback = 255; + // Intentional fall-through + case COLUMN_TYPES.JET_coltypLongText: + wrapper.column.cbMax = cbMax || fallback || 64 * 1024; + break; + case COLUMN_TYPES.JET_coltypGUID: + wrapper.column.cbMax = 16; + break; + case COLUMN_TYPES.JET_coltypBit: + wrapper.column.cbMax = 1; + break; + case COLUMN_TYPES.JET_coltypLongLong: + wrapper.column.cbMax = 8; + break; + default: + throw new Error("Unknown column type!"); + } + + wrapper.column.columnid = new ESE.JET_COLUMNID(); + wrapper.column.grbit = 0; + wrapper.column.pvDefault = null; + wrapper.column.cbDefault = 0; + wrapper.column.cp = 0; + + return wrapper; +} + +// "forward declarations" of indexcreate and setinfo structs, which we don't use. +ESE.JET_INDEXCREATE = new ctypes.StructType("JET_INDEXCREATE"); +ESE.JET_SETINFO = new ctypes.StructType("JET_SETINFO"); + +ESE.JET_TABLECREATE_W = new ctypes.StructType("JET_TABLECREATE_W", [ + {"cbStruct": ctypes.unsigned_long}, + {"szTableName": ESE.JET_PCWSTR}, + {"szTemplateTableName": ESE.JET_PCWSTR}, + {"ulPages": ctypes.unsigned_long}, + {"ulDensity": ctypes.unsigned_long}, + {"rgcolumncreate": ESE.JET_COLUMNCREATE_W.ptr}, + {"cColumns": ctypes.unsigned_long}, + {"rgindexcreate": ESE.JET_INDEXCREATE.ptr}, + {"cIndexes": ctypes.unsigned_long}, + {"grbit": ESE.JET_GRBIT}, + {"tableid": ESE.JET_TABLEID}, + {"cCreated": ctypes.unsigned_long}, +]); + +function createTableCreationWrapper(tableName, columns) { + let wrapper = {}; + let wchar_tArray = ctypes.ArrayType(ctypes.char16_t); + wrapper.name = new wchar_tArray(tableName.length + 1); + wrapper.name.value = String(tableName); + wrapper.table = new ESE.JET_TABLECREATE_W(); + wrapper.table.cbStruct = ESE.JET_TABLECREATE_W.size; + wrapper.table.szTableName = wrapper.name; + wrapper.table.szTemplateTableName = null; + wrapper.table.ulPages = 1; + wrapper.table.ulDensity = 0; + let columnArrayType = ESE.JET_COLUMNCREATE_W.array(columns.length); + wrapper.columnAry = new columnArrayType(); + wrapper.table.rgcolumncreate = wrapper.columnAry.addressOfElement(0); + wrapper.table.cColumns = columns.length; + wrapper.columns = []; + for (let i = 0; i < columns.length; i++) { + let column = columns[i]; + let columnWrapper = createColumnCreationWrapper(column); + wrapper.columnAry.addressOfElement(i).contents = columnWrapper.column; + wrapper.columns.push(columnWrapper); + } + wrapper.table.rgindexcreate = null; + wrapper.table.cIndexes = 0; + return wrapper; +} + +function convertValueForWriting(value, valueType) { + let buffer; + let valueOfValueType = ctypes.UInt64.lo(valueType); + switch (valueOfValueType) { + case COLUMN_TYPES.JET_coltypLongLong: + if (value instanceof Date) { + buffer = new KERNEL.FILETIME(); + let sysTime = new KERNEL.SYSTEMTIME(); + sysTime.wYear = value.getUTCFullYear(); + sysTime.wMonth = value.getUTCMonth() + 1; + sysTime.wDay = value.getUTCDate(); + sysTime.wHour = value.getUTCHours(); + sysTime.wMinute = value.getUTCMinutes(); + sysTime.wSecond = value.getUTCSeconds(); + sysTime.wMilliseconds = value.getUTCMilliseconds(); + let rv = KERNEL.SystemTimeToFileTime(sysTime.address(), buffer.address()); + if (!rv) { + throw new Error("Failed to get FileTime."); + } + return [buffer, KERNEL.FILETIME.size]; + } + throw new Error("Unrecognized value for longlong column"); + case COLUMN_TYPES.JET_coltypLongText: + let wchar_tArray = ctypes.ArrayType(ctypes.char16_t); + buffer = new wchar_tArray(value.length + 1); + buffer.value = String(value); + return [buffer, buffer.length * 2]; + case COLUMN_TYPES.JET_coltypBit: + buffer = new ctypes.uint8_t(); + // Bizarre boolean values, but whatever: + buffer.value = value ? 255 : 0; + return [buffer, 1]; + case COLUMN_TYPES.JET_coltypGUID: + let byteArray = ctypes.ArrayType(ctypes.uint8_t); + buffer = new byteArray(16); + let j = 0; + for (let i = 0; i < value.length; i++) { + if (!(/[0-9a-f]/i).test(value[i])) { + continue; + } + let byteAsHex = value.substr(i, 2); + buffer[j++] = parseInt(byteAsHex, 16); + i++; + } + return [buffer, 16]; + } + + throw new Error("Unknown type " + valueType); +} + +let initializedESE = false; + +let eseDBWritingHelpers = { + setupDB(dbFile, tableName, columns, rows) { + if (!initializedESE) { + initializedESE = true; + loadLibraries(); + + KERNEL.SystemTimeToFileTime = gLibs.kernel.declare("SystemTimeToFileTime", + ctypes.default_abi, ctypes.bool, KERNEL.SYSTEMTIME.ptr, KERNEL.FILETIME.ptr); + + declareESEFunction("CreateDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR, + ESE.JET_PCWSTR, ESE.JET_DBID.ptr, ESE.JET_GRBIT); + declareESEFunction("CreateTableColumnIndexW", ESE.JET_SESID, ESE.JET_DBID, + ESE.JET_TABLECREATE_W.ptr); + declareESEFunction("BeginTransaction", ESE.JET_SESID); + declareESEFunction("CommitTransaction", ESE.JET_SESID, ESE.JET_GRBIT); + declareESEFunction("PrepareUpdate", ESE.JET_SESID, ESE.JET_TABLEID, + ctypes.unsigned_long); + declareESEFunction("Update", ESE.JET_SESID, ESE.JET_TABLEID, + ctypes.voidptr_t, ctypes.unsigned_long, + ctypes.unsigned_long.ptr); + declareESEFunction("SetColumn", ESE.JET_SESID, ESE.JET_TABLEID, + ESE.JET_COLUMNID, ctypes.voidptr_t, + ctypes.unsigned_long, ESE.JET_GRBIT, ESE.JET_SETINFO.ptr); + ESE.SetSystemParameterW(null, 0, 64 /* JET_paramDatabasePageSize*/, + 8192, null); + } + + let rootPath = dbFile.parent.path + "\\"; + let logPath = rootPath + "LogFiles\\"; + + try { + this._instanceId = new ESE.JET_INSTANCE(); + ESE.CreateInstanceW(this._instanceId.address(), + "firefox-dbwriter-" + (gESEInstanceCounter++)); + this._instanceCreated = true; + + ESE.SetSystemParameterW(this._instanceId.address(), 0, + 0 /* JET_paramSystemPath*/, 0, rootPath); + ESE.SetSystemParameterW(this._instanceId.address(), 0, + 1 /* JET_paramTempPath */, 0, rootPath); + ESE.SetSystemParameterW(this._instanceId.address(), 0, + 2 /* JET_paramLogFilePath*/, 0, logPath); + // Shouldn't try to call JetTerm if the following call fails. + this._instanceCreated = false; + ESE.Init(this._instanceId.address()); + this._instanceCreated = true; + this._sessionId = new ESE.JET_SESID(); + ESE.BeginSessionW(this._instanceId, this._sessionId.address(), null, + null); + this._sessionCreated = true; + + this._dbId = new ESE.JET_DBID(); + this._dbPath = rootPath + "spartan.edb"; + ESE.CreateDatabaseW(this._sessionId, this._dbPath, null, + this._dbId.address(), 0); + this._opened = this._attached = true; + + let tableCreationWrapper = createTableCreationWrapper(tableName, columns); + ESE.CreateTableColumnIndexW(this._sessionId, this._dbId, + tableCreationWrapper.table.address()); + this._tableId = tableCreationWrapper.table.tableid; + + let columnIdMap = new Map(); + if (rows.length) { + // Iterate over the struct we passed into ESENT because they have the + // created column ids. + let columnCount = ctypes.UInt64.lo(tableCreationWrapper.table.cColumns); + let columnsPassed = tableCreationWrapper.table.rgcolumncreate; + for (let i = 0; i < columnCount; i++) { + let column = columnsPassed.contents; + columnIdMap.set(column.szColumnName.readString(), column); + columnsPassed = columnsPassed.increment(); + } + ESE.ManualMove(this._sessionId, this._tableId, + -2147483648 /* JET_MoveFirst */, 0); + ESE.BeginTransaction(this._sessionId); + for (let row of rows) { + ESE.PrepareUpdate(this._sessionId, this._tableId, 0 /* JET_prepInsert */); + for (let columnName in row) { + let col = columnIdMap.get(columnName); + let colId = col.columnid; + let [val, valSize] = convertValueForWriting(row[columnName], col.coltyp); + /* JET_bitSetOverwriteLV */ + ESE.SetColumn(this._sessionId, this._tableId, colId, val.address(), valSize, 4, null); + } + let actualBookmarkSize = new ctypes.unsigned_long(); + ESE.Update(this._sessionId, this._tableId, null, 0, actualBookmarkSize.address()); + } + ESE.CommitTransaction(this._sessionId, 0 /* JET_bitWaitLastLevel0Commit */); + } + } finally { + try { + this._close(); + } catch (ex) { + Cu.reportError(ex); + } + } + }, + + _close() { + if (this._tableId) { + ESE.FailSafeCloseTable(this._sessionId, this._tableId); + delete this._tableId; + } + if (this._opened) { + ESE.FailSafeCloseDatabase(this._sessionId, this._dbId, 0); + this._opened = false; + } + if (this._attached) { + ESE.FailSafeDetachDatabaseW(this._sessionId, this._dbPath); + this._attached = false; + } + if (this._sessionCreated) { + ESE.FailSafeEndSession(this._sessionId, 0); + this._sessionCreated = false; + } + if (this._instanceCreated) { + ESE.FailSafeTerm(this._instanceId); + this._instanceCreated = false; + } + }, +}; + +add_task(function*() { + let tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile); + tempFile.append("fx-xpcshell-edge-db"); + tempFile.createUnique(tempFile.DIRECTORY_TYPE, 0o600); + + let db = tempFile.clone(); + db.append("spartan.edb"); + + let logs = tempFile.clone(); + logs.append("LogFiles"); + logs.create(tempFile.DIRECTORY_TYPE, 0o600); + + let creationDate = new Date(Date.now() - 5000); + const kEdgeMenuParent = "62d07e2b-5f0d-4e41-8426-5f5ec9717beb"; + let itemsInDB = [ + { + URL: "http://www.mozilla.org/", + Title: "Mozilla", + DateUpdated: new Date(creationDate.valueOf() + 100), + ItemId: "1c00c10a-15f6-4618-92dd-22575102a4da", + ParentId: kEdgeMenuParent, + IsFolder: false, + IsDeleted: false, + }, + { + Title: "Folder", + DateUpdated: new Date(creationDate.valueOf() + 200), + ItemId: "564b21f2-05d6-4f7d-8499-304d00ccc3aa", + ParentId: kEdgeMenuParent, + IsFolder: true, + IsDeleted: false, + }, + { + Title: "Item in folder", + URL: "http://www.iteminfolder.org/", + DateUpdated: new Date(creationDate.valueOf() + 300), + ItemId: "c295ddaf-04a1-424a-866c-0ebde011e7c8", + ParentId: "564b21f2-05d6-4f7d-8499-304d00ccc3aa", + IsFolder: false, + IsDeleted: false, + }, + { + Title: "Deleted folder", + DateUpdated: new Date(creationDate.valueOf() + 400), + ItemId: "a547573c-4d4d-4406-a736-5b5462d93bca", + ParentId: kEdgeMenuParent, + IsFolder: true, + IsDeleted: true, + }, + { + Title: "Deleted item", + URL: "http://www.deleteditem.org/", + DateUpdated: new Date(creationDate.valueOf() + 500), + ItemId: "37a574bb-b44b-4bbc-a414-908615536435", + ParentId: kEdgeMenuParent, + IsFolder: false, + IsDeleted: true, + }, + { + Title: "Item in deleted folder (should be in root)", + URL: "http://www.itemindeletedfolder.org/", + DateUpdated: new Date(creationDate.valueOf() + 600), + ItemId: "74dd1cc3-4c5d-471f-bccc-7bc7c72fa621", + ParentId: "a547573c-4d4d-4406-a736-5b5462d93bca", + IsFolder: false, + IsDeleted: false, + }, + { + Title: "_Favorites_Bar_", + DateUpdated: new Date(creationDate.valueOf() + 700), + ItemId: "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf", + ParentId: kEdgeMenuParent, + IsFolder: true, + IsDeleted: false, + }, + { + Title: "Item in favorites bar", + URL: "http://www.iteminfavoritesbar.org/", + DateUpdated: new Date(creationDate.valueOf() + 800), + ItemId: "9f2b1ff8-b651-46cf-8f41-16da8bcb6791", + ParentId: "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf", + IsFolder: false, + IsDeleted: false, + }, + ]; + eseDBWritingHelpers.setupDB(db, "Favorites", [ + {type: COLUMN_TYPES.JET_coltypLongText, name: "URL", cbMax: 4096}, + {type: COLUMN_TYPES.JET_coltypLongText, name: "Title", cbMax: 4096}, + {type: COLUMN_TYPES.JET_coltypLongLong, name: "DateUpdated"}, + {type: COLUMN_TYPES.JET_coltypGUID, name: "ItemId"}, + {type: COLUMN_TYPES.JET_coltypBit, name: "IsDeleted"}, + {type: COLUMN_TYPES.JET_coltypBit, name: "IsFolder"}, + {type: COLUMN_TYPES.JET_coltypGUID, name: "ParentId"}, + ], itemsInDB); + + let migrator = Cc["@mozilla.org/profile/migrator;1?app=browser&type=edge"] + .createInstance(Ci.nsIBrowserProfileMigrator); + let bookmarksMigrator = migrator.wrappedJSObject.getESEMigratorForTesting(db); + Assert.ok(bookmarksMigrator.exists, "Should recognize table we just created"); + + let source = MigrationUtils.getLocalizedString("sourceNameEdge"); + let sourceLabel = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]); + + let seenBookmarks = []; + let bookmarkObserver = { + onItemAdded(itemId, parentId, index, itemType, url, title, dateAdded, itemGuid, parentGuid) { + if (title.startsWith("Deleted")) { + ok(false, "Should not see deleted items being bookmarked!"); + } + seenBookmarks.push({itemId, parentId, index, itemType, url, title, dateAdded, itemGuid, parentGuid}); + }, + onBeginUpdateBatch() {}, + onEndUpdateBatch() {}, + onItemRemoved() {}, + onItemChanged() {}, + onItemVisited() {}, + onItemMoved() {}, + }; + PlacesUtils.bookmarks.addObserver(bookmarkObserver, false); + + let migrateResult = yield new Promise(resolve => bookmarksMigrator.migrate(resolve)).catch(ex => { + Cu.reportError(ex); + Assert.ok(false, "Got an exception trying to migrate data! " + ex); + return false; + }); + PlacesUtils.bookmarks.removeObserver(bookmarkObserver); + Assert.ok(migrateResult, "Migration should succeed"); + Assert.equal(seenBookmarks.length, 7, "Should have seen 7 items being bookmarked."); + Assert.equal(seenBookmarks.filter(bm => bm.title != sourceLabel).length, + MigrationUtils._importQuantities.bookmarks, + "Telemetry should have items except for 'From Microsoft Edge' folders"); + + let menuParents = seenBookmarks.filter(item => item.parentGuid == PlacesUtils.bookmarks.menuGuid); + Assert.equal(menuParents.length, 1, "Should have a single folder added to the menu"); + let toolbarParents = seenBookmarks.filter(item => item.parentGuid == PlacesUtils.bookmarks.toolbarGuid); + Assert.equal(toolbarParents.length, 1, "Should have a single item added to the toolbar"); + let menuParentGuid = menuParents[0].itemGuid; + let toolbarParentGuid = toolbarParents[0].itemGuid; + + let expectedTitlesInMenu = itemsInDB.filter(item => item.ParentId == kEdgeMenuParent).map(item => item.Title); + // Hacky, but seems like much the simplest way: + expectedTitlesInMenu.push("Item in deleted folder (should be in root)"); + let expectedTitlesInToolbar = itemsInDB.filter(item => item.ParentId == "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf").map(item => item.Title); + + let edgeNameStr = MigrationUtils.getLocalizedString("sourceNameEdge"); + let importParentFolderName = MigrationUtils.getLocalizedString("importedBookmarksFolder", [edgeNameStr]); + + for (let bookmark of seenBookmarks) { + let shouldBeInMenu = expectedTitlesInMenu.includes(bookmark.title); + let shouldBeInToolbar = expectedTitlesInToolbar.includes(bookmark.title); + if (bookmark.title == "Folder" || bookmark.title == importParentFolderName) { + Assert.equal(bookmark.itemType, PlacesUtils.bookmarks.TYPE_FOLDER, + "Bookmark " + bookmark.title + " should be a folder"); + } else { + Assert.notEqual(bookmark.itemType, PlacesUtils.bookmarks.TYPE_FOLDER, + "Bookmark " + bookmark.title + " should not be a folder"); + } + + if (shouldBeInMenu) { + Assert.equal(bookmark.parentGuid, menuParentGuid, "Item '" + bookmark.title + "' should be in menu"); + } else if (shouldBeInToolbar) { + Assert.equal(bookmark.parentGuid, toolbarParentGuid, "Item '" + bookmark.title + "' should be in toolbar"); + } else if (bookmark.itemGuid == menuParentGuid || bookmark.itemGuid == toolbarParentGuid) { + Assert.ok(true, "Expect toolbar and menu folders to not be in menu or toolbar"); + } else { + // Bit hacky, but we do need to check this. + Assert.equal(bookmark.title, "Item in folder", "Subfoldered item shouldn't be in menu or toolbar"); + let parent = seenBookmarks.find(maybeParent => maybeParent.itemGuid == bookmark.parentGuid); + Assert.equal(parent && parent.title, "Folder", "Subfoldered item should be in subfolder labeled 'Folder'"); + } + + let dbItem = itemsInDB.find(someItem => bookmark.title == someItem.Title); + if (!dbItem) { + Assert.equal(bookmark.title, importParentFolderName, "Only the extra layer of folders isn't in the input we stuck in the DB."); + Assert.ok([menuParentGuid, toolbarParentGuid].includes(bookmark.itemGuid), "This item should be one of the containers"); + } else { + Assert.equal(dbItem.URL || null, bookmark.url && bookmark.url.spec, "URL is correct"); + Assert.equal(dbItem.DateUpdated.valueOf(), (new Date(bookmark.dateAdded / 1000)).valueOf(), "Date added is correct"); + } + } +}); + diff --git a/browser/components/migration/tests/unit/test_IE7_passwords.js b/browser/components/migration/tests/unit/test_IE7_passwords.js new file mode 100644 index 000000000..1ce016a7d --- /dev/null +++ b/browser/components/migration/tests/unit/test_IE7_passwords.js @@ -0,0 +1,397 @@ +"use strict"; + +Cu.import("resource://gre/modules/AppConstants.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry", + "resource://gre/modules/WindowsRegistry.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto", + "resource://gre/modules/OSCrypto.jsm"); + +const IE7_FORM_PASSWORDS_MIGRATOR_NAME = "IE7FormPasswords"; +const LOGINS_KEY = "Software\\Microsoft\\Internet Explorer\\IntelliForms\\Storage2"; +const EXTENSION = "-backup"; +const TESTED_WEBSITES = { + twitter: { + uri: makeURI("https://twitter.com"), + hash: "A89D42BC6406E27265B1AD0782B6F376375764A301", + data: [12, 0, 0, 0, 56, 0, 0, 0, 38, 0, 0, 0, 87, 73, 67, 75, 24, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68, 36, 67, 124, 118, 212, 208, 1, 8, 0, 0, 0, 18, 0, 0, 0, 68, 36, 67, 124, 118, 212, 208, 1, 9, 0, 0, 0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102, 0, 103, 0, 104, 0, 0, 0, 49, 0, 50, 0, 51, 0, 52, 0, 53, 0, 54, 0, 55, 0, 56, 0, 57, 0, 0, 0], + logins: [ + { + username: "abcdefgh", + password: "123456789", + hostname: "https://twitter.com", + formSubmitURL: "", + httpRealm: null, + usernameField: "", + passwordField: "", + timeCreated: 1439325854000, + timeLastUsed: 1439325854000, + timePasswordChanged: 1439325854000, + timesUsed: 1, + }, + ], + }, + facebook: { + uri: makeURI("https://www.facebook.com/"), + hash: "EF44D3E034009CB0FD1B1D81A1FF3F3335213BD796", + data: [12, 0, 0, 0, 152, 0, 0, 0, 160, 0, 0, 0, 87, 73, 67, 75, 24, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 88, 182, 125, 18, 121, 212, 208, 1, 9, 0, 0, 0, 20, 0, 0, 0, 88, 182, 125, 18, 121, 212, 208, 1, 9, 0, 0, 0, 40, 0, 0, 0, 134, 65, 33, 37, 121, 212, 208, 1, 9, 0, 0, 0, 60, 0, 0, 0, 134, 65, 33, 37, 121, 212, 208, 1, 9, 0, 0, 0, 80, 0, 0, 0, 45, 242, 246, 62, 121, 212, 208, 1, 9, 0, 0, 0, 100, 0, 0, 0, 45, 242, 246, 62, 121, 212, 208, 1, 9, 0, 0, 0, 120, 0, 0, 0, 28, 10, 193, 80, 121, 212, 208, 1, 9, 0, 0, 0, 140, 0, 0, 0, 28, 10, 193, 80, 121, 212, 208, 1, 9, 0, 0, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 48, 0, 0, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 48, 0, 0, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 49, 0, 0, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 49, 0, 0, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 50, 0, 0, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 50, 0, 0, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 51, 0, 0, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 51, 0, 0, 0], + logins: [ + { + username: "username0", + password: "password0", + hostname: "https://www.facebook.com", + formSubmitURL: "", + httpRealm: null, + usernameField: "", + passwordField: "", + timeCreated: 1439326966000, + timeLastUsed: 1439326966000, + timePasswordChanged: 1439326966000, + timesUsed: 1, + }, + { + username: "username1", + password: "password1", + hostname: "https://www.facebook.com", + formSubmitURL: "", + httpRealm: null, + usernameField: "", + passwordField: "", + timeCreated: 1439326997000, + timeLastUsed: 1439326997000, + timePasswordChanged: 1439326997000, + timesUsed: 1, + }, + { + username: "username2", + password: "password2", + hostname: "https://www.facebook.com", + formSubmitURL: "", + httpRealm: null, + usernameField: "", + passwordField: "", + timeCreated: 1439327040000, + timeLastUsed: 1439327040000, + timePasswordChanged: 1439327040000, + timesUsed: 1, + }, + { + username: "username3", + password: "password3", + hostname: "https://www.facebook.com", + formSubmitURL: "", + httpRealm: null, + usernameField: "", + passwordField: "", + timeCreated: 1439327070000, + timeLastUsed: 1439327070000, + timePasswordChanged: 1439327070000, + timesUsed: 1, + }, + ], + }, + live: { + uri: makeURI("https://login.live.com/"), + hash: "7B506F2D6B81D939A8E0456F036EE8970856FF705E", + data: [12, 0, 0, 0, 56, 0, 0, 0, 44, 0, 0, 0, 87, 73, 67, 75, 24, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 212, 17, 219, 140, 148, 212, 208, 1, 9, 0, 0, 0, 20, 0, 0, 0, 212, 17, 219, 140, 148, 212, 208, 1, 11, 0, 0, 0, 114, 0, 105, 0, 97, 0, 100, 0, 104, 0, 49, 6, 74, 6, 39, 6, 54, 6, 0, 0, 39, 6, 66, 6, 49, 6, 35, 6, 80, 0, 192, 0, 223, 0, 119, 0, 246, 0, 114, 0, 100, 0, 0, 0], + logins: [ + { + username: "riadhرياض", + password: "اقرأPÀßwörd", + hostname: "https://login.live.com", + formSubmitURL: "", + httpRealm: null, + usernameField: "", + passwordField: "", + timeCreated: 1439338767000, + timeLastUsed: 1439338767000, + timePasswordChanged: 1439338767000, + timesUsed: 1, + }, + ], + }, + reddit: { + uri: makeURI("http://www.reddit.com/"), + hash: "B644028D1C109A91EC2C4B9D1F145E55A1FAE42065", + data: [12, 0, 0, 0, 152, 0, 0, 0, 212, 0, 0, 0, 87, 73, 67, 75, 24, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 8, 234, 114, 153, 212, 208, 1, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 97, 93, 131, 116, 153, 212, 208, 1, 3, 0, 0, 0, 14, 0, 0, 0, 97, 93, 131, 116, 153, 212, 208, 1, 16, 0, 0, 0, 48, 0, 0, 0, 88, 150, 78, 174, 153, 212, 208, 1, 4, 0, 0, 0, 58, 0, 0, 0, 88, 150, 78, 174, 153, 212, 208, 1, 29, 0, 0, 0, 118, 0, 0, 0, 79, 102, 137, 34, 154, 212, 208, 1, 15, 0, 0, 0, 150, 0, 0, 0, 79, 102, 137, 34, 154, 212, 208, 1, 30, 0, 0, 0, 97, 0, 0, 0, 0, 0, 252, 140, 173, 138, 146, 48, 0, 0, 66, 0, 105, 0, 116, 0, 116, 0, 101, 0, 32, 0, 98, 0, 101, 0, 115, 0, 116, 0, 228, 0, 116, 0, 105, 0, 103, 0, 101, 0, 110, 0, 0, 0, 205, 145, 110, 127, 198, 91, 1, 120, 0, 0, 31, 4, 48, 4, 64, 4, 62, 4, 59, 4, 76, 4, 32, 0, 67, 4, 65, 4, 63, 4, 53, 4, 72, 4, 61, 4, 62, 4, 32, 0, 65, 4, 49, 4, 64, 4, 62, 4, 72, 4, 53, 4, 61, 4, 46, 0, 32, 0, 18, 4, 62, 4, 57, 4, 66, 4, 56, 4, 0, 0, 40, 6, 51, 6, 69, 6, 32, 0, 39, 6, 68, 6, 68, 6, 71, 6, 32, 0, 39, 6, 68, 6, 49, 6, 45, 6, 69, 6, 70, 6, 0, 0, 118, 0, 101, 0, 117, 0, 105, 0, 108, 0, 108, 0, 101, 0, 122, 0, 32, 0, 108, 0, 101, 0, 32, 0, 118, 0, 233, 0, 114, 0, 105, 0, 102, 0, 105, 0, 101, 0, 114, 0, 32, 0, 224, 0, 32, 0, 110, 0, 111, 0, 117, 0, 118, 0, 101, 0, 97, 0, 117, 0, 0, 0], + logins: [ + { + username: "購読を", + password: "Bitte bestätigen", + hostname: "http://www.reddit.com", + formSubmitURL: "", + httpRealm: null, + usernameField: "", + passwordField: "", + timeCreated: 1439340874000, + timeLastUsed: 1439340874000, + timePasswordChanged: 1439340874000, + timesUsed: 1, + }, + { + username: "重置密码", + password: "Пароль успешно сброшен. Войти", + hostname: "http://www.reddit.com", + formSubmitURL: "", + httpRealm: null, + usernameField: "", + passwordField: "", + timeCreated: 1439340971000, + timeLastUsed: 1439340971000, + timePasswordChanged: 1439340971000, + timesUsed: 1, + }, + { + username: "بسم الله الرحمن", + password: "veuillez le vérifier à nouveau", + hostname: "http://www.reddit.com", + formSubmitURL: "", + httpRealm: null, + usernameField: "", + passwordField: "", + timeCreated: 1439341166000, + timeLastUsed: 1439341166000, + timePasswordChanged: 1439341166000, + timesUsed: 1, + }, + ], + }, +}; + +const TESTED_URLS = [ + "http://a.foo.com", + "http://b.foo.com", + "http://c.foo.com", + "http://www.test.net", + "http://www.test.net/home", + "http://www.test.net/index", + "https://a.bar.com", + "https://b.bar.com", + "https://c.bar.com", +]; + +var nsIWindowsRegKey = Ci.nsIWindowsRegKey; +var Storage2Key; + +/* + * If the key value exists, it's going to be backed up and replaced, so the value could be restored. + * Otherwise a new value is going to be created. + */ +function backupAndStore(key, name, value) { + if (key.hasValue(name)) { + // backup the the current value + let type = key.getValueType(name); + // create a new value using use the current value name followed by EXTENSION as its new name + switch (type) { + case nsIWindowsRegKey.TYPE_STRING: + key.writeStringValue(name + EXTENSION, key.readStringValue(name)); + break; + case nsIWindowsRegKey.TYPE_BINARY: + key.writeBinaryValue(name + EXTENSION, key.readBinaryValue(name)); + break; + case nsIWindowsRegKey.TYPE_INT: + key.writeIntValue(name + EXTENSION, key.readIntValue(name)); + break; + case nsIWindowsRegKey.TYPE_INT64: + key.writeInt64Value(name + EXTENSION, key.readInt64Value(name)); + break; + } + } + key.writeBinaryValue(name, value); +} + +// Remove all values where their names are members of the names array from the key of registry +function removeAllValues(key, names) { + for (let name of names) { + key.removeValue(name); + } +} + +// Restore all the backed up values +function restore(key) { + let count = key.valueCount; + let names = []; // the names of the key values + for (let i = 0; i < count; ++i) { + names.push(key.getValueName(i)); + } + + for (let name of names) { + // backed up values have EXTENSION at the end of their names + if (name.lastIndexOf(EXTENSION) == name.length - EXTENSION.length) { + let valueName = name.substr(0, name.length - EXTENSION.length); + let type = key.getValueType(name); + // create a new value using the name before the backup and removed the backed up one + switch (type) { + case nsIWindowsRegKey.TYPE_STRING: + key.writeStringValue(valueName, key.readStringValue(name)); + key.removeValue(name); + break; + case nsIWindowsRegKey.TYPE_BINARY: + key.writeBinaryValue(valueName, key.readBinaryValue(name)); + key.removeValue(name); + break; + case nsIWindowsRegKey.TYPE_INT: + key.writeIntValue(valueName, key.readIntValue(name)); + key.removeValue(name); + break; + case nsIWindowsRegKey.TYPE_INT64: + key.writeInt64Value(valueName, key.readInt64Value(name)); + key.removeValue(name); + break; + } + } + } +} + +function checkLoginsAreEqual(passwordManagerLogin, IELogin, id) { + passwordManagerLogin.QueryInterface(Ci.nsILoginMetaInfo); + for (let attribute in IELogin) { + Assert.equal(passwordManagerLogin[attribute], IELogin[attribute], + "The two logins ID " + id + " have the same " + attribute); + } +} + +function createRegistryPath(path) { + let loginPath = path.split("\\"); + let parentKey = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(nsIWindowsRegKey); + let currentPath = []; + for (let currentKey of loginPath) { + parentKey.open(nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, currentPath.join("\\"), + nsIWindowsRegKey.ACCESS_ALL); + + if (!parentKey.hasChild(currentKey)) { + parentKey.createChild(currentKey, 0); + } + currentPath.push(currentKey); + parentKey.close(); + } +} + +function getFirstResourceOfType(type) { + let migrator = Cc["@mozilla.org/profile/migrator;1?app=browser&type=ie"] + .createInstance(Ci.nsISupports) + .wrappedJSObject; + let migrators = migrator.getResources(); + for (let m of migrators) { + if (m.name == IE7_FORM_PASSWORDS_MIGRATOR_NAME && m.type == type) { + return m; + } + } + throw new Error("failed to find the " + type + " migrator"); +} + +function makeURI(aURL) { + return Services.io.newURI(aURL, null, null); +} + +add_task(function* setup() { + if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) { + Assert.throws(() => getFirstResourceOfType(MigrationUtils.resourceTypes.PASSWORDS), + "The migrator doesn't exist for win8+"); + return; + } + // create the path to Storage2 in the registry if it doest exist. + createRegistryPath(LOGINS_KEY); + Storage2Key = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(nsIWindowsRegKey); + Storage2Key.open(nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, LOGINS_KEY, + nsIWindowsRegKey.ACCESS_ALL); + + // create a dummy value otherwise the migrator doesn't exist + if (!Storage2Key.hasValue("dummy")) { + Storage2Key.writeBinaryValue("dummy", "dummy"); + } +}); + +add_task(function* test_passwordsNotAvailable() { + if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) { + return; + } + + let migrator = getFirstResourceOfType(MigrationUtils.resourceTypes.PASSWORDS); + Assert.ok(migrator.exists, "The migrator has to exist"); + let logins = Services.logins.getAllLogins({}); + Assert.equal(logins.length, 0, "There are no logins at the beginning of the test"); + + let uris = []; // the uris of the migrated logins + for (let url of TESTED_URLS) { + uris.push(makeURI(url)); + // in this test, there is no IE login data in the registry, so after the migration, the number + // of logins in the store should be 0 + migrator._migrateURIs(uris); + logins = Services.logins.getAllLogins({}); + Assert.equal(logins.length, 0, + "There are no logins after doing the migration without adding values to the registry"); + } +}); + +add_task(function* test_passwordsAvailable() { + if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) { + return; + } + + let crypto = new OSCrypto(); + let hashes = []; // the hashes of all migrator websites, this is going to be used for the clean up + + do_register_cleanup(() => { + Services.logins.removeAllLogins(); + logins = Services.logins.getAllLogins({}); + Assert.equal(logins.length, 0, "There are no logins after the cleanup"); + // remove all the values created in this test from the registry + removeAllValues(Storage2Key, hashes); + // restore all backed up values + restore(Storage2Key); + + // clean the dummy value + if (Storage2Key.hasValue("dummy")) { + Storage2Key.removeValue("dummy"); + } + Storage2Key.close(); + crypto.finalize(); + }); + + let migrator = getFirstResourceOfType(MigrationUtils.resourceTypes.PASSWORDS); + Assert.ok(migrator.exists, "The migrator has to exist"); + let logins = Services.logins.getAllLogins({}); + Assert.equal(logins.length, 0, "There are no logins at the beginning of the test"); + + let uris = []; // the uris of the migrated logins + + let loginCount = 0; + for (let current in TESTED_WEBSITES) { + let website = TESTED_WEBSITES[current]; + // backup the current the registry value if it exists and replace the existing value/create a + // new value with the encrypted data + backupAndStore(Storage2Key, website.hash, + crypto.encryptData(crypto.arrayToString(website.data), + website.uri.spec, true)); + Assert.ok(migrator.exists, "The migrator has to exist"); + uris.push(website.uri); + hashes.push(website.hash); + + migrator._migrateURIs(uris); + logins = Services.logins.getAllLogins({}); + // check that the number of logins in the password manager has increased as expected which means + // that all the values for the current website were imported + loginCount += website.logins.length; + Assert.equal(logins.length, loginCount, + "The number of logins has increased after the migration"); + // NB: because telemetry records any login data passed to the login manager, it + // also gets told about logins that are duplicates or invalid (for one reason + // or another) and so its counts might exceed those of the login manager itself. + Assert.greaterOrEqual(MigrationUtils._importQuantities.logins, loginCount, + "Telemetry quantities equal or exceed the actual import."); + // Reset - this normally happens at the start of a new migration, but we're calling + // the migrator directly so can't rely on that: + MigrationUtils._importQuantities.logins = 0; + + let startIndex = loginCount - website.logins.length; + // compares the imported password manager logins with their expected logins + for (let i = 0; i < website.logins.length; i++) { + checkLoginsAreEqual(logins[startIndex + i], website.logins[i], + " " + current + " - " + i + " "); + } + } +}); diff --git a/browser/components/migration/tests/unit/test_IE_bookmarks.js b/browser/components/migration/tests/unit/test_IE_bookmarks.js new file mode 100644 index 000000000..a166c0502 --- /dev/null +++ b/browser/components/migration/tests/unit/test_IE_bookmarks.js @@ -0,0 +1,44 @@ +"use strict"; + +add_task(function* () { + let migrator = MigrationUtils.getMigrator("ie"); + // Sanity check for the source. + Assert.ok(migrator.sourceExists); + + // Wait for the imported bookmarks. Check that "From Internet Explorer" + // folders are created in the menu and on the toolbar. + let source = MigrationUtils.getLocalizedString("sourceNameIE"); + let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]); + + let expectedParents = [ PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.toolbarFolderId ]; + + let itemCount = 0; + let bmObserver = { + onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle) { + if (aTitle != label) { + itemCount++; + } + if (expectedParents.length > 0 && aTitle == label) { + let index = expectedParents.indexOf(aParentId); + Assert.notEqual(index, -1); + expectedParents.splice(index, 1); + } + }, + onBeginUpdateBatch() {}, + onEndUpdateBatch() {}, + onItemRemoved() {}, + onItemChanged() {}, + onItemVisited() {}, + onItemMoved() {}, + }; + PlacesUtils.bookmarks.addObserver(bmObserver, false); + + yield promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS); + PlacesUtils.bookmarks.removeObserver(bmObserver); + Assert.equal(MigrationUtils._importQuantities.bookmarks, itemCount, + "Ensure telemetry matches actual number of imported items."); + + // Check the bookmarks have been imported to all the expected parents. + Assert.equal(expectedParents.length, 0, "Got all the expected parents"); +}); diff --git a/browser/components/migration/tests/unit/test_IE_cookies.js b/browser/components/migration/tests/unit/test_IE_cookies.js new file mode 100644 index 000000000..37a7462f2 --- /dev/null +++ b/browser/components/migration/tests/unit/test_IE_cookies.js @@ -0,0 +1,111 @@ +"use strict"; + +XPCOMUtils.defineLazyModuleGetter(this, "ctypes", + "resource://gre/modules/ctypes.jsm"); + +add_task(function* () { + let migrator = MigrationUtils.getMigrator("ie"); + // Sanity check for the source. + Assert.ok(migrator.sourceExists); + + const BOOL = ctypes.bool; + const LPCTSTR = ctypes.char16_t.ptr; + const DWORD = ctypes.uint32_t; + const LPDWORD = DWORD.ptr; + + let wininet = ctypes.open("Wininet"); + + /* + BOOL InternetSetCookieW( + _In_ LPCTSTR lpszUrl, + _In_ LPCTSTR lpszCookieName, + _In_ LPCTSTR lpszCookieData + ); + */ + let setIECookie = wininet.declare("InternetSetCookieW", + ctypes.default_abi, + BOOL, + LPCTSTR, + LPCTSTR, + LPCTSTR); + + /* + BOOL InternetGetCookieW( + _In_ LPCTSTR lpszUrl, + _In_ LPCTSTR lpszCookieName, + _Out_ LPCTSTR lpszCookieData, + _Inout_ LPDWORD lpdwSize + ); + */ + let getIECookie = wininet.declare("InternetGetCookieW", + ctypes.default_abi, + BOOL, + LPCTSTR, + LPCTSTR, + LPCTSTR, + LPDWORD); + + // We need to randomize the cookie to avoid clashing with other cookies + // that might have been set by previous tests and not properly cleared. + let date = (new Date()).getDate(); + const COOKIE = { + get host() { + return new URL(this.href).host; + }, + href: `http://mycookietest.${Math.random()}.com`, + name: "testcookie", + value: "testvalue", + expiry: new Date(new Date().setDate(date + 2)) + }; + let data = ctypes.char16_t.array()(256); + let sizeRef = DWORD(256).address(); + + do_register_cleanup(() => { + // Remove the cookie. + try { + let expired = new Date(new Date().setDate(date - 2)); + let rv = setIECookie(COOKIE.href, COOKIE.name, + `; expires=${expired.toUTCString()}`); + Assert.ok(rv, "Expired the IE cookie"); + Assert.ok(!getIECookie(COOKIE.href, COOKIE.name, data, sizeRef), + "The cookie has been properly removed"); + } catch (ex) {} + + // Close the library. + try { + wininet.close(); + } catch (ex) {} + }); + + // Create the persistent cookie in IE. + let value = `${COOKIE.value}; expires=${COOKIE.expiry.toUTCString()}`; + let rv = setIECookie(COOKIE.href, COOKIE.name, value); + Assert.ok(rv, "Added a persistent IE cookie: " + value); + + // Sanity check the cookie has been created. + Assert.ok(getIECookie(COOKIE.href, COOKIE.name, data, sizeRef), + "Found the added persistent IE cookie"); + do_print("Found cookie: " + data.readString()); + Assert.equal(data.readString(), `${COOKIE.name}=${COOKIE.value}`, + "Found the expected cookie"); + + // Sanity check that there are no cookies. + Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 0, + "There are no cookies initially"); + + // Migrate cookies. + yield promiseMigration(migrator, MigrationUtils.resourceTypes.COOKIES); + + Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 1, + "Migrated the expected number of cookies"); + + // Now check the cookie details. + let enumerator = Services.cookies.getCookiesFromHost(COOKIE.host, {}); + Assert.ok(enumerator.hasMoreElements()); + let foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2); + + Assert.equal(foundCookie.name, COOKIE.name); + Assert.equal(foundCookie.value, COOKIE.value); + Assert.equal(foundCookie.host, "." + COOKIE.host); + Assert.equal(foundCookie.expiry, Math.floor(COOKIE.expiry / 1000)); +}); diff --git a/browser/components/migration/tests/unit/test_Safari_bookmarks.js b/browser/components/migration/tests/unit/test_Safari_bookmarks.js new file mode 100644 index 000000000..edc32dc72 --- /dev/null +++ b/browser/components/migration/tests/unit/test_Safari_bookmarks.js @@ -0,0 +1,46 @@ +"use strict"; + +add_task(function* () { + registerFakePath("ULibDir", do_get_file("Library/")); + + let migrator = MigrationUtils.getMigrator("safari"); + // Sanity check for the source. + Assert.ok(migrator.sourceExists); + + // Wait for the imported bookmarks. Check that "From Safari" + // folders are created on the toolbar. + let source = MigrationUtils.getLocalizedString("sourceNameSafari"); + let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]); + + let expectedParents = [ PlacesUtils.toolbarFolderId ]; + let itemCount = 0; + + let bmObserver = { + onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle) { + if (aTitle != label) { + itemCount++; + } + if (expectedParents.length > 0 && aTitle == label) { + let index = expectedParents.indexOf(aParentId); + Assert.ok(index != -1, "Found expected parent"); + expectedParents.splice(index, 1); + } + }, + onBeginUpdateBatch() {}, + onEndUpdateBatch() {}, + onItemRemoved() {}, + onItemChanged() {}, + onItemVisited() {}, + onItemMoved() {}, + }; + PlacesUtils.bookmarks.addObserver(bmObserver, false); + + yield promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS); + PlacesUtils.bookmarks.removeObserver(bmObserver); + + // Check the bookmarks have been imported to all the expected parents. + Assert.ok(!expectedParents.length, "No more expected parents"); + Assert.equal(itemCount, 13, "Should import all 13 items."); + // Check that the telemetry matches: + Assert.equal(MigrationUtils._importQuantities.bookmarks, itemCount, "Telemetry reporting correct."); +}); diff --git a/browser/components/migration/tests/unit/test_automigration.js b/browser/components/migration/tests/unit/test_automigration.js new file mode 100644 index 000000000..bc9076a6c --- /dev/null +++ b/browser/components/migration/tests/unit/test_automigration.js @@ -0,0 +1,695 @@ +"use strict"; + +let AutoMigrateBackstage = Cu.import("resource:///modules/AutoMigrate.jsm"); /* globals AutoMigrate */ + +let gShimmedMigratorKeyPicker = null; +let gShimmedMigrator = null; + +const kUsecPerMin = 60 * 1000000; + +// This is really a proxy on MigrationUtils, but if we specify that directly, +// we get in trouble because the object itself is frozen, and Proxies can't +// return a different value to an object when directly proxying a frozen +// object. +AutoMigrateBackstage.MigrationUtils = new Proxy({}, { + get(target, name) { + if (name == "getMigratorKeyForDefaultBrowser" && gShimmedMigratorKeyPicker) { + return gShimmedMigratorKeyPicker; + } + if (name == "getMigrator" && gShimmedMigrator) { + return function() { return gShimmedMigrator }; + } + return MigrationUtils[name]; + }, +}); + +do_register_cleanup(function() { + AutoMigrateBackstage.MigrationUtils = MigrationUtils; +}); + +// This should be replaced by using History.fetch with a fetchVisits option, +// once that becomes available +function* visitsForURL(url) +{ + let visitCount = 0; + let db = yield PlacesUtils.promiseDBConnection(); + visitCount = yield db.execute( + `SELECT count(*) FROM moz_historyvisits v + JOIN moz_places h ON h.id = v.place_id + WHERE url_hash = hash(:url) AND url = :url`, + {url}); + visitCount = visitCount[0].getInt64(0); + return visitCount; +} + + +/** + * Test automatically picking a browser to migrate from + */ +add_task(function* checkMigratorPicking() { + Assert.throws(() => AutoMigrate.pickMigrator("firefox"), + /Can't automatically migrate from Firefox/, + "Should throw when explicitly picking Firefox."); + + Assert.throws(() => AutoMigrate.pickMigrator("gobbledygook"), + /migrator object is not available/, + "Should throw when passing unknown migrator key"); + gShimmedMigratorKeyPicker = function() { + return "firefox"; + }; + Assert.throws(() => AutoMigrate.pickMigrator(), + /Can't automatically migrate from Firefox/, + "Should throw when implicitly picking Firefox."); + gShimmedMigratorKeyPicker = function() { + return "gobbledygook"; + }; + Assert.throws(() => AutoMigrate.pickMigrator(), + /migrator object is not available/, + "Should throw when an unknown migrator is the default"); + gShimmedMigratorKeyPicker = function() { + return ""; + }; + Assert.throws(() => AutoMigrate.pickMigrator(), + /Could not determine default browser key/, + "Should throw when an unknown migrator is the default"); +}); + + +/** + * Test automatically picking a profile to migrate from + */ +add_task(function* checkProfilePicking() { + let fakeMigrator = {sourceProfiles: [{id: "a"}, {id: "b"}]}; + let profB = fakeMigrator.sourceProfiles[1]; + Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator), + /Don't know how to pick a profile when more/, + "Should throw when there are multiple profiles."); + Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator, "c"), + /Profile specified was not found/, + "Should throw when the profile supplied doesn't exist."); + let profileToMigrate = AutoMigrate.pickProfile(fakeMigrator, "b"); + Assert.equal(profileToMigrate, profB, "Should return profile supplied"); + + fakeMigrator.sourceProfiles = null; + Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator, "c"), + /Profile specified but only a default profile found./, + "Should throw when the profile supplied doesn't exist."); + profileToMigrate = AutoMigrate.pickProfile(fakeMigrator); + Assert.equal(profileToMigrate, null, "Should return default profile when that's the only one."); + + fakeMigrator.sourceProfiles = []; + Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator), + /No profile data found/, + "Should throw when no profile data is present."); + + fakeMigrator.sourceProfiles = [{id: "a"}]; + let profA = fakeMigrator.sourceProfiles[0]; + profileToMigrate = AutoMigrate.pickProfile(fakeMigrator); + Assert.equal(profileToMigrate, profA, "Should return the only profile if only one is present."); +}); + +/** + * Test the complete automatic process including browser and profile selection, + * and actual migration (which implies startup) + */ +add_task(function* checkIntegration() { + gShimmedMigrator = { + get sourceProfiles() { + do_print("Read sourceProfiles"); + return null; + }, + getMigrateData(profileToMigrate) { + this._getMigrateDataArgs = profileToMigrate; + return Ci.nsIBrowserProfileMigrator.BOOKMARKS; + }, + migrate(types, startup, profileToMigrate) { + this._migrateArgs = [types, startup, profileToMigrate]; + }, + }; + gShimmedMigratorKeyPicker = function() { + return "gobbledygook"; + }; + AutoMigrate.migrate("startup"); + Assert.strictEqual(gShimmedMigrator._getMigrateDataArgs, null, + "getMigrateData called with 'null' as a profile"); + + let {BOOKMARKS, HISTORY, PASSWORDS} = Ci.nsIBrowserProfileMigrator; + let expectedTypes = BOOKMARKS | HISTORY | PASSWORDS; + Assert.deepEqual(gShimmedMigrator._migrateArgs, [expectedTypes, "startup", null], + "migrate called with 'null' as a profile"); +}); + +/** + * Test the undo preconditions and a no-op undo in the automigrator. + */ +add_task(function* checkUndoPreconditions() { + let shouldAddData = false; + gShimmedMigrator = { + get sourceProfiles() { + do_print("Read sourceProfiles"); + return null; + }, + getMigrateData(profileToMigrate) { + this._getMigrateDataArgs = profileToMigrate; + return Ci.nsIBrowserProfileMigrator.BOOKMARKS; + }, + migrate(types, startup, profileToMigrate) { + this._migrateArgs = [types, startup, profileToMigrate]; + if (shouldAddData) { + // Insert a login and check that that worked. + MigrationUtils.insertLoginWrapper({ + hostname: "www.mozilla.org", + formSubmitURL: "http://www.mozilla.org", + username: "user", + password: "pass", + }); + } + TestUtils.executeSoon(function() { + Services.obs.notifyObservers(null, "Migration:Ended", undefined); + }); + }, + }; + + gShimmedMigratorKeyPicker = function() { + return "gobbledygook"; + }; + AutoMigrate.migrate("startup"); + let migrationFinishedPromise = TestUtils.topicObserved("Migration:Ended"); + Assert.strictEqual(gShimmedMigrator._getMigrateDataArgs, null, + "getMigrateData called with 'null' as a profile"); + + let {BOOKMARKS, HISTORY, PASSWORDS} = Ci.nsIBrowserProfileMigrator; + let expectedTypes = BOOKMARKS | HISTORY | PASSWORDS; + Assert.deepEqual(gShimmedMigrator._migrateArgs, [expectedTypes, "startup", null], + "migrate called with 'null' as a profile"); + + yield migrationFinishedPromise; + Assert.ok(Preferences.has("browser.migrate.automigrate.browser"), + "Should have set browser pref"); + Assert.ok(!(yield AutoMigrate.canUndo()), "Should not be able to undo migration, as there's no data"); + gShimmedMigrator._migrateArgs = null; + gShimmedMigrator._getMigrateDataArgs = null; + Preferences.reset("browser.migrate.automigrate.browser"); + shouldAddData = true; + + AutoMigrate.migrate("startup"); + migrationFinishedPromise = TestUtils.topicObserved("Migration:Ended"); + Assert.strictEqual(gShimmedMigrator._getMigrateDataArgs, null, + "getMigrateData called with 'null' as a profile"); + Assert.deepEqual(gShimmedMigrator._migrateArgs, [expectedTypes, "startup", null], + "migrate called with 'null' as a profile"); + + yield migrationFinishedPromise; + let storedLogins = Services.logins.findLogins({}, "www.mozilla.org", + "http://www.mozilla.org", null); + Assert.equal(storedLogins.length, 1, "Should have 1 login"); + + Assert.ok(Preferences.has("browser.migrate.automigrate.browser"), + "Should have set browser pref"); + Assert.ok((yield AutoMigrate.canUndo()), "Should be able to undo migration, as now there's data"); + + yield AutoMigrate.undo(); + Assert.ok(true, "Should be able to finish an undo cycle."); + + // Check that the undo removed the passwords: + storedLogins = Services.logins.findLogins({}, "www.mozilla.org", + "http://www.mozilla.org", null); + Assert.equal(storedLogins.length, 0, "Should have no logins"); +}); + +/** + * Fake a migration and then try to undo it to verify all data gets removed. + */ +add_task(function* checkUndoRemoval() { + MigrationUtils.initializeUndoData(); + Preferences.set("browser.migrate.automigrate.browser", "automationbrowser"); + // Insert a login and check that that worked. + MigrationUtils.insertLoginWrapper({ + hostname: "www.mozilla.org", + formSubmitURL: "http://www.mozilla.org", + username: "user", + password: "pass", + }); + let storedLogins = Services.logins.findLogins({}, "www.mozilla.org", + "http://www.mozilla.org", null); + Assert.equal(storedLogins.length, 1, "Should have 1 login"); + + // Insert a bookmark and check that we have exactly 1 bookmark for that URI. + yield MigrationUtils.insertBookmarkWrapper({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: "http://www.example.org/", + title: "Some example bookmark", + }); + + let bookmark = yield PlacesUtils.bookmarks.fetch({url: "http://www.example.org/"}); + Assert.ok(bookmark, "Should have a bookmark before undo"); + Assert.equal(bookmark.title, "Some example bookmark", "Should have correct bookmark before undo."); + + // Insert 2 history visits + let now_uSec = Date.now() * 1000; + let visitedURI = Services.io.newURI("http://www.example.com/", null, null); + let frecencyUpdatePromise = new Promise(resolve => { + let expectedChanges = 2; + let observer = { + onFrecencyChanged: function() { + if (!--expectedChanges) { + PlacesUtils.history.removeObserver(observer); + resolve(); + } + }, + }; + PlacesUtils.history.addObserver(observer, false); + }); + yield MigrationUtils.insertVisitsWrapper([{ + uri: visitedURI, + visits: [ + { + transitionType: PlacesUtils.history.TRANSITION_LINK, + visitDate: now_uSec, + }, + { + transitionType: PlacesUtils.history.TRANSITION_LINK, + visitDate: now_uSec - 100 * kUsecPerMin, + }, + ] + }]); + yield frecencyUpdatePromise; + + // Verify that both visits get reported. + let opts = PlacesUtils.history.getNewQueryOptions(); + opts.resultType = opts.RESULTS_AS_VISIT; + let query = PlacesUtils.history.getNewQuery(); + query.uri = visitedURI; + let visits = PlacesUtils.history.executeQuery(query, opts); + visits.root.containerOpen = true; + Assert.equal(visits.root.childCount, 2, "Should have 2 visits"); + // Clean up: + visits.root.containerOpen = false; + + yield AutoMigrate.saveUndoState(); + + // Verify that we can undo, then undo: + Assert.ok(AutoMigrate.canUndo(), "Should be possible to undo migration"); + yield AutoMigrate.undo(); + + let histograms = [ + "FX_STARTUP_MIGRATION_UNDO_BOOKMARKS_ERRORCOUNT", + "FX_STARTUP_MIGRATION_UNDO_LOGINS_ERRORCOUNT", + "FX_STARTUP_MIGRATION_UNDO_VISITS_ERRORCOUNT", + ]; + for (let histogramId of histograms) { + let keyedHistogram = Services.telemetry.getKeyedHistogramById(histogramId); + let histogramData = keyedHistogram.snapshot().automationbrowser; + Assert.equal(histogramData.sum, 0, `Should have reported 0 errors to ${histogramId}.`); + Assert.greaterOrEqual(histogramData.counts[0], 1, `Should have reported value of 0 one time to ${histogramId}.`); + } + histograms = [ + "FX_STARTUP_MIGRATION_UNDO_BOOKMARKS_MS", + "FX_STARTUP_MIGRATION_UNDO_LOGINS_MS", + "FX_STARTUP_MIGRATION_UNDO_VISITS_MS", + "FX_STARTUP_MIGRATION_UNDO_TOTAL_MS", + ]; + for (let histogramId of histograms) { + Assert.greater(Services.telemetry.getKeyedHistogramById(histogramId).snapshot().automationbrowser.sum, 0, + `Should have reported non-zero time spent using undo for ${histogramId}`); + } + + // Check that the undo removed the history visits: + visits = PlacesUtils.history.executeQuery(query, opts); + visits.root.containerOpen = true; + Assert.equal(visits.root.childCount, 0, "Should have no more visits"); + visits.root.containerOpen = false; + + // Check that the undo removed the bookmarks: + bookmark = yield PlacesUtils.bookmarks.fetch({url: "http://www.example.org/"}); + Assert.ok(!bookmark, "Should have no bookmarks after undo"); + + // Check that the undo removed the passwords: + storedLogins = Services.logins.findLogins({}, "www.mozilla.org", + "http://www.mozilla.org", null); + Assert.equal(storedLogins.length, 0, "Should have no logins"); +}); + +add_task(function* checkUndoBookmarksState() { + MigrationUtils.initializeUndoData(); + const {TYPE_FOLDER, TYPE_BOOKMARK} = PlacesUtils.bookmarks; + let title = "Some example bookmark"; + let url = "http://www.example.com"; + let parentGuid = PlacesUtils.bookmarks.toolbarGuid; + let {guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({ + title, url, parentGuid + }); + Assert.deepEqual((yield MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks"), + [{lastModified, parentGuid, guid, type: TYPE_BOOKMARK}]); + + MigrationUtils.initializeUndoData(); + ({guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({ + title, parentGuid, type: TYPE_FOLDER + })); + let folder = {guid, lastModified, parentGuid, type: TYPE_FOLDER}; + let folderGuid = folder.guid; + ({guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({ + title, url, parentGuid: folderGuid + })); + let kid1 = {guid, lastModified, parentGuid: folderGuid, type: TYPE_BOOKMARK}; + ({guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({ + title, url, parentGuid: folderGuid + })); + let kid2 = {guid, lastModified, parentGuid: folderGuid, type: TYPE_BOOKMARK}; + + let bookmarksUndo = (yield MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks"); + Assert.equal(bookmarksUndo.length, 3); + // We expect that the last modified time from first kid #1 and then kid #2 + // has been propagated to the folder: + folder.lastModified = kid2.lastModified; + // Not just using deepEqual on the entire array (which should work) because + // the failure messages get truncated by xpcshell which is unhelpful. + Assert.deepEqual(bookmarksUndo[0], folder); + Assert.deepEqual(bookmarksUndo[1], kid1); + Assert.deepEqual(bookmarksUndo[2], kid2); + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* testBookmarkRemovalByUndo() { + const {TYPE_FOLDER} = PlacesUtils.bookmarks; + MigrationUtils.initializeUndoData(); + let title = "Some example bookmark"; + let url = "http://www.mymagicaluniqueurl.com"; + let parentGuid = PlacesUtils.bookmarks.toolbarGuid; + let {guid} = yield MigrationUtils.insertBookmarkWrapper({ + title: "Some folder", parentGuid, type: TYPE_FOLDER + }); + let folderGuid = guid; + let itemsToRemove = []; + ({guid} = yield MigrationUtils.insertBookmarkWrapper({ + title: "Inner folder", parentGuid: folderGuid, type: TYPE_FOLDER + })); + let innerFolderGuid = guid; + itemsToRemove.push(innerFolderGuid); + + ({guid} = yield MigrationUtils.insertBookmarkWrapper({ + title: "Inner inner folder", parentGuid: innerFolderGuid, type: TYPE_FOLDER + })); + itemsToRemove.push(guid); + + ({guid} = yield MigrationUtils.insertBookmarkWrapper({ + title: "Inner nested item", url: "http://inner-nested-example.com", parentGuid: guid + })); + itemsToRemove.push(guid); + + ({guid} = yield MigrationUtils.insertBookmarkWrapper({ + title, url, parentGuid: folderGuid + })); + itemsToRemove.push(guid); + + for (let toBeRemovedGuid of itemsToRemove) { + let dbResultForGuid = yield PlacesUtils.bookmarks.fetch(toBeRemovedGuid); + Assert.ok(dbResultForGuid, "Should be able to find items that will be removed."); + } + let bookmarkUndoState = (yield MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks"); + // Now insert a separate item into this folder, not related to the migration. + let newItem = yield PlacesUtils.bookmarks.insert( + {title: "Not imported", parentGuid: folderGuid, url: "http://www.example.com"} + ); + + yield AutoMigrate._removeUnchangedBookmarks(bookmarkUndoState); + Assert.ok(true, "Successfully removed imported items."); + + let itemFromDB = yield PlacesUtils.bookmarks.fetch(newItem.guid); + Assert.ok(itemFromDB, "Item we inserted outside of migration is still there."); + itemFromDB = yield PlacesUtils.bookmarks.fetch(folderGuid); + Assert.ok(itemFromDB, "Folder we inserted in migration is still there because of new kids."); + for (let removedGuid of itemsToRemove) { + let dbResultForGuid = yield PlacesUtils.bookmarks.fetch(removedGuid); + let dbgStr = dbResultForGuid && dbResultForGuid.title; + Assert.equal(null, dbResultForGuid, "Should not be able to find items that should have been removed, but found " + dbgStr); + } + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* checkUndoLoginsState() { + MigrationUtils.initializeUndoData(); + MigrationUtils.insertLoginWrapper({ + username: "foo", + password: "bar", + hostname: "https://example.com", + formSubmitURL: "https://example.com/", + timeCreated: new Date(), + }); + let storedLogins = Services.logins.findLogins({}, "https://example.com", "", ""); + let storedLogin = storedLogins[0]; + storedLogin.QueryInterface(Ci.nsILoginMetaInfo); + let {guid, timePasswordChanged} = storedLogin; + let undoLoginData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("logins"); + Assert.deepEqual([{guid, timePasswordChanged}], undoLoginData); + Services.logins.removeAllLogins(); +}); + +add_task(function* testLoginsRemovalByUndo() { + MigrationUtils.initializeUndoData(); + MigrationUtils.insertLoginWrapper({ + username: "foo", + password: "bar", + hostname: "https://example.com", + formSubmitURL: "https://example.com/", + timeCreated: new Date(), + }); + MigrationUtils.insertLoginWrapper({ + username: "foo", + password: "bar", + hostname: "https://example.org", + formSubmitURL: "https://example.org/", + timeCreated: new Date(new Date().getTime() - 10000), + }); + // This should update the existing login + LoginHelper.maybeImportLogin({ + username: "foo", + password: "bazzy", + hostname: "https://example.org", + formSubmitURL: "https://example.org/", + timePasswordChanged: new Date(), + }); + Assert.equal(1, LoginHelper.searchLoginsWithObject({hostname: "https://example.org", formSubmitURL: "https://example.org/"}).length, + "Should be only 1 login for example.org (that was updated)"); + let undoLoginData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("logins"); + + yield AutoMigrate._removeUnchangedLogins(undoLoginData); + Assert.equal(0, LoginHelper.searchLoginsWithObject({hostname: "https://example.com", formSubmitURL: "https://example.com/"}).length, + "unchanged example.com entry should have been removed."); + Assert.equal(1, LoginHelper.searchLoginsWithObject({hostname: "https://example.org", formSubmitURL: "https://example.org/"}).length, + "changed example.org entry should have persisted."); + Services.logins.removeAllLogins(); +}); + +add_task(function* checkUndoVisitsState() { + MigrationUtils.initializeUndoData(); + yield MigrationUtils.insertVisitsWrapper([{ + uri: NetUtil.newURI("http://www.example.com/"), + title: "Example", + visits: [{ + visitDate: new Date("2015-07-10").getTime() * 1000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK, + }, { + visitDate: new Date("2015-09-10").getTime() * 1000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK, + }, { + visitDate: new Date("2015-08-10").getTime() * 1000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK, + }], + }, { + uri: NetUtil.newURI("http://www.example.org/"), + title: "Example", + visits: [{ + visitDate: new Date("2016-04-03").getTime() * 1000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK, + }, { + visitDate: new Date("2015-08-03").getTime() * 1000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK, + }], + }, { + uri: NetUtil.newURI("http://www.example.com/"), + title: "Example", + visits: [{ + visitDate: new Date("2015-10-10").getTime() * 1000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK, + }], + }]); + let undoVisitData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("visits"); + Assert.deepEqual(Array.from(undoVisitData.map(v => v.url)).sort(), + ["http://www.example.com/", "http://www.example.org/"]); + Assert.deepEqual(undoVisitData.find(v => v.url == "http://www.example.com/"), { + url: "http://www.example.com/", + visitCount: 4, + first: new Date("2015-07-10").getTime() * 1000, + last: new Date("2015-10-10").getTime() * 1000, + }); + Assert.deepEqual(undoVisitData.find(v => v.url == "http://www.example.org/"), { + url: "http://www.example.org/", + visitCount: 2, + first: new Date("2015-08-03").getTime() * 1000, + last: new Date("2016-04-03").getTime() * 1000, + }); + + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* checkUndoVisitsState() { + MigrationUtils.initializeUndoData(); + yield MigrationUtils.insertVisitsWrapper([{ + uri: NetUtil.newURI("http://www.example.com/"), + title: "Example", + visits: [{ + visitDate: new Date("2015-07-10").getTime() * 1000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK, + }, { + visitDate: new Date("2015-09-10").getTime() * 1000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK, + }, { + visitDate: new Date("2015-08-10").getTime() * 1000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK, + }], + }, { + uri: NetUtil.newURI("http://www.example.org/"), + title: "Example", + visits: [{ + visitDate: new Date("2016-04-03").getTime() * 1000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK, + }, { + visitDate: new Date("2015-08-03").getTime() * 1000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK, + }], + }, { + uri: NetUtil.newURI("http://www.example.com/"), + title: "Example", + visits: [{ + visitDate: new Date("2015-10-10").getTime() * 1000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK, + }], + }, { + uri: NetUtil.newURI("http://www.mozilla.org/"), + title: "Example", + visits: [{ + visitDate: new Date("2015-01-01").getTime() * 1000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK, + }], + }]); + + // We have to wait until frecency updates have been handled in order + // to accurately determine whether we're doing the right thing. + let frecencyUpdatesHandled = new Promise(resolve => { + PlacesUtils.history.addObserver({ + onFrecencyChanged(aURI) { + if (aURI.spec == "http://www.unrelated.org/") { + PlacesUtils.history.removeObserver(this); + resolve(); + } + } + }, false); + }); + yield PlacesUtils.history.insertMany([{ + url: "http://www.example.com/", + title: "Example", + visits: [{ + date: new Date("2015-08-16"), + }], + }, { + url: "http://www.example.org/", + title: "Example", + visits: [{ + date: new Date("2016-01-03"), + }, { + date: new Date("2015-05-03"), + }], + }, { + url: "http://www.unrelated.org/", + title: "Unrelated", + visits: [{ + date: new Date("2015-09-01"), + }], + }]); + yield frecencyUpdatesHandled; + let undoVisitData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("visits"); + + let frecencyChangesExpected = new Map([ + ["http://www.example.com/", PromiseUtils.defer()], + ["http://www.example.org/", PromiseUtils.defer()] + ]); + let uriDeletedExpected = new Map([ + ["http://www.mozilla.org/", PromiseUtils.defer()], + ]); + let wrongMethodDeferred = PromiseUtils.defer(); + let observer = { + onBeginUpdateBatch: function() {}, + onEndUpdateBatch: function() {}, + onVisit: function(uri) { + wrongMethodDeferred.reject(new Error("Unexpected call to onVisit " + uri.spec)); + }, + onTitleChanged: function(uri) { + wrongMethodDeferred.reject(new Error("Unexpected call to onTitleChanged " + uri.spec)); + }, + onClearHistory: function() { + wrongMethodDeferred.reject("Unexpected call to onClearHistory"); + }, + onPageChanged: function(uri) { + wrongMethodDeferred.reject(new Error("Unexpected call to onPageChanged " + uri.spec)); + }, + onFrecencyChanged: function(aURI) { + do_print("frecency change"); + Assert.ok(frecencyChangesExpected.has(aURI.spec), + "Should be expecting frecency change for " + aURI.spec); + frecencyChangesExpected.get(aURI.spec).resolve(); + }, + onManyFrecenciesChanged: function() { + do_print("Many frecencies changed"); + wrongMethodDeferred.reject(new Error("This test can't deal with onManyFrecenciesChanged to be called")); + }, + onDeleteURI: function(aURI) { + do_print("delete uri"); + Assert.ok(uriDeletedExpected.has(aURI.spec), + "Should be expecting uri deletion for " + aURI.spec); + uriDeletedExpected.get(aURI.spec).resolve(); + }, + }; + PlacesUtils.history.addObserver(observer, false); + + yield AutoMigrate._removeSomeVisits(undoVisitData); + PlacesUtils.history.removeObserver(observer); + yield Promise.all(uriDeletedExpected.values()); + yield Promise.all(frecencyChangesExpected.values()); + + Assert.equal(yield visitsForURL("http://www.example.com/"), 1, + "1 example.com visit (out of 5) should have persisted despite being within the range, due to limiting"); + Assert.equal(yield visitsForURL("http://www.mozilla.org/"), 0, + "0 mozilla.org visits should have persisted (out of 1)."); + Assert.equal(yield visitsForURL("http://www.example.org/"), 2, + "2 example.org visits should have persisted (out of 4)."); + Assert.equal(yield visitsForURL("http://www.unrelated.org/"), 1, + "1 unrelated.org visits should have persisted as it's not involved in the import."); + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* checkHistoryRemovalCompletion() { + AutoMigrate._errorMap = {bookmarks: 0, visits: 0, logins: 0}; + yield AutoMigrate._removeSomeVisits([{url: "http://www.example.com/", limit: -1}]); + ok(true, "Removing visits should complete even if removing some visits failed."); + Assert.equal(AutoMigrate._errorMap.visits, 1, "Should have logged the error for visits."); + + // Unfortunately there's not a reliable way to make removing bookmarks be + // unhappy unless the DB is messed up (e.g. contains children but has + // parents removed already). + yield AutoMigrate._removeUnchangedBookmarks([ + {guid: PlacesUtils.bookmarks, lastModified: new Date(0), parentGuid: 0}, + {guid: "gobbledygook", lastModified: new Date(0), parentGuid: 0}, + ]); + ok(true, "Removing bookmarks should complete even if some items are gone or bogus."); + Assert.equal(AutoMigrate._errorMap.bookmarks, 0, + "Should have ignored removing non-existing (or builtin) bookmark."); + + + yield AutoMigrate._removeUnchangedLogins([ + {guid: "gobbledygook", timePasswordChanged: new Date(0)}, + ]); + ok(true, "Removing logins should complete even if logins don't exist."); + Assert.equal(AutoMigrate._errorMap.logins, 0, + "Should have ignored removing non-existing logins."); +}); diff --git a/browser/components/migration/tests/unit/test_fx_telemetry.js b/browser/components/migration/tests/unit/test_fx_telemetry.js new file mode 100644 index 000000000..a276f52f8 --- /dev/null +++ b/browser/components/migration/tests/unit/test_fx_telemetry.js @@ -0,0 +1,288 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* globals do_get_tempdir */ + +"use strict"; + +function run_test() { + run_next_test(); +} + +function readFile(file) { + let stream = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + stream.init(file, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF); + + let sis = Cc["@mozilla.org/scriptableinputstream;1"] + .createInstance(Ci.nsIScriptableInputStream); + sis.init(stream); + let contents = sis.read(file.fileSize); + sis.close(); + return contents; +} + +function checkDirectoryContains(dir, files) { + print("checking " + dir.path + " - should contain " + Object.keys(files)); + let seen = new Set(); + let enumerator = dir.directoryEntries; + while (enumerator.hasMoreElements()) { + let file = enumerator.getNext().QueryInterface(Ci.nsIFile); + print("found file: " + file.path); + Assert.ok(file.leafName in files, file.leafName + " exists, but shouldn't"); + + let expectedContents = files[file.leafName]; + if (typeof expectedContents != "string") { + // it's a subdir - recurse! + Assert.ok(file.isDirectory(), "should be a subdir"); + let newDir = dir.clone(); + newDir.append(file.leafName); + checkDirectoryContains(newDir, expectedContents); + } else { + Assert.ok(!file.isDirectory(), "should be a regular file"); + let contents = readFile(file); + Assert.equal(contents, expectedContents); + } + seen.add(file.leafName); + } + let missing = []; + for (let x in files) { + if (!seen.has(x)) { + missing.push(x); + } + } + Assert.deepEqual(missing, [], "no missing files in " + dir.path); +} + +function getTestDirs() { + // we make a directory structure in a temp dir which mirrors what we are + // testing. + let tempDir = do_get_tempdir(); + let srcDir = tempDir.clone(); + srcDir.append("test_source_dir"); + srcDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + + let targetDir = tempDir.clone(); + targetDir.append("test_target_dir"); + targetDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + + // no need to cleanup these dirs - the xpcshell harness will do it for us. + return [srcDir, targetDir]; +} + +function writeToFile(dir, leafName, contents) { + let file = dir.clone(); + file.append(leafName); + + let outputStream = FileUtils.openFileOutputStream(file); + outputStream.write(contents, contents.length); + outputStream.close(); +} + +function createSubDir(dir, subDirName) { + let subDir = dir.clone(); + subDir.append(subDirName); + subDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + return subDir; +} + +function promiseMigrator(name, srcDir, targetDir) { + let migrator = Cc["@mozilla.org/profile/migrator;1?app=browser&type=firefox"] + .createInstance(Ci.nsISupports) + .wrappedJSObject; + let migrators = migrator._getResourcesInternal(srcDir, targetDir); + for (let m of migrators) { + if (m.name == name) { + return new Promise(resolve => m.migrate(resolve)); + } + } + throw new Error("failed to find the " + name + " migrator"); +} + +function promiseTelemetryMigrator(srcDir, targetDir) { + return promiseMigrator("telemetry", srcDir, targetDir); +} + +add_task(function* test_empty() { + let [srcDir, targetDir] = getTestDirs(); + let ok = yield promiseTelemetryMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true with empty directories"); + // check both are empty + checkDirectoryContains(srcDir, {}); + checkDirectoryContains(targetDir, {}); +}); + +add_task(function* test_migrate_files() { + let [srcDir, targetDir] = getTestDirs(); + + // Set up datareporting files, some to copy, some not. + let stateContent = JSON.stringify({ + clientId: "68d5474e-19dc-45c1-8e9a-81fca592707c", + }); + let sessionStateContent = "foobar 5432"; + let subDir = createSubDir(srcDir, "datareporting"); + writeToFile(subDir, "state.json", stateContent); + writeToFile(subDir, "session-state.json", sessionStateContent); + writeToFile(subDir, "other.file", "do not copy"); + + let archived = createSubDir(subDir, "archived"); + writeToFile(archived, "other.file", "do not copy"); + + // Set up FHR files, they should not be copied. + writeToFile(srcDir, "healthreport.sqlite", "do not copy"); + writeToFile(srcDir, "healthreport.sqlite-wal", "do not copy"); + subDir = createSubDir(srcDir, "healthreport"); + writeToFile(subDir, "state.json", "do not copy"); + writeToFile(subDir, "other.file", "do not copy"); + + // Perform migration. + let ok = yield promiseTelemetryMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true with important telemetry files copied"); + + checkDirectoryContains(targetDir, { + "datareporting": { + "state.json": stateContent, + "session-state.json": sessionStateContent, + }, + }); +}); + +add_task(function* test_fallback_fhr_state() { + let [srcDir, targetDir] = getTestDirs(); + + // Test that we fall back to migrating FHR state if the datareporting + // state file does not exist. + let stateContent = JSON.stringify({ + clientId: "68d5474e-19dc-45c1-8e9a-81fca592707c", + }); + let subDir = createSubDir(srcDir, "healthreport"); + writeToFile(subDir, "state.json", stateContent); + + // Perform migration. + let ok = yield promiseTelemetryMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true"); + + checkDirectoryContains(targetDir, { + "healthreport": { + "state.json": stateContent, + }, + }); +}); + + +add_task(function* test_datareporting_not_dir() { + let [srcDir, targetDir] = getTestDirs(); + + writeToFile(srcDir, "datareporting", "I'm a file but should be a directory"); + + let ok = yield promiseTelemetryMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true even though the directory was a file"); + + checkDirectoryContains(targetDir, {}); +}); + +add_task(function* test_datareporting_empty() { + let [srcDir, targetDir] = getTestDirs(); + + // Migrate with an empty 'datareporting' subdir. + createSubDir(srcDir, "datareporting"); + let ok = yield promiseTelemetryMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true"); + + // We should end up with no migrated files. + checkDirectoryContains(targetDir, { + "datareporting": {}, + }); +}); + +add_task(function* test_healthreport_empty() { + let [srcDir, targetDir] = getTestDirs(); + + // Migrate with no 'datareporting' and an empty 'healthreport' subdir. + createSubDir(srcDir, "healthreport"); + let ok = yield promiseTelemetryMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true"); + + // We should end up with no migrated files. + checkDirectoryContains(targetDir, {}); +}); + +add_task(function* test_datareporting_many() { + let [srcDir, targetDir] = getTestDirs(); + + // Create some datareporting files. + let subDir = createSubDir(srcDir, "datareporting"); + let shouldBeCopied = "should be copied"; + writeToFile(subDir, "state.json", shouldBeCopied); + writeToFile(subDir, "session-state.json", shouldBeCopied); + writeToFile(subDir, "something.else", "should not"); + createSubDir(subDir, "emptyDir"); + + let ok = yield promiseTelemetryMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true"); + + checkDirectoryContains(targetDir, { + "datareporting" : { + "state.json": shouldBeCopied, + "session-state.json": shouldBeCopied, + } + }); +}); + +add_task(function* test_no_session_state() { + let [srcDir, targetDir] = getTestDirs(); + + // Check that migration still works properly if we only have state.json. + let subDir = createSubDir(srcDir, "datareporting"); + let stateContent = "abcd984"; + writeToFile(subDir, "state.json", stateContent); + + let ok = yield promiseTelemetryMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true"); + + checkDirectoryContains(targetDir, { + "datareporting" : { + "state.json": stateContent, + } + }); +}); + +add_task(function* test_no_state() { + let [srcDir, targetDir] = getTestDirs(); + + // Check that migration still works properly if we only have session-state.json. + let subDir = createSubDir(srcDir, "datareporting"); + let sessionStateContent = "abcd512"; + writeToFile(subDir, "session-state.json", sessionStateContent); + + let ok = yield promiseTelemetryMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true"); + + checkDirectoryContains(targetDir, { + "datareporting" : { + "session-state.json": sessionStateContent, + } + }); +}); + +add_task(function* test_times_migration() { + let [srcDir, targetDir] = getTestDirs(); + + // create a times.json in the source directory. + let contents = JSON.stringify({created: 1234}); + writeToFile(srcDir, "times.json", contents); + + let earliest = Date.now(); + let ok = yield promiseMigrator("times", srcDir, targetDir); + Assert.ok(ok, "callback should have been true"); + let latest = Date.now(); + + let timesFile = targetDir.clone(); + timesFile.append("times.json"); + + let raw = readFile(timesFile); + let times = JSON.parse(raw); + Assert.ok(times.reset >= earliest && times.reset <= latest); + // and it should have left the creation time alone. + Assert.equal(times.created, 1234); +}); diff --git a/browser/components/migration/tests/unit/xpcshell.ini b/browser/components/migration/tests/unit/xpcshell.ini new file mode 100644 index 000000000..1b9f0a5f1 --- /dev/null +++ b/browser/components/migration/tests/unit/xpcshell.ini @@ -0,0 +1,26 @@ +[DEFAULT] +head = head_migration.js +tail = +firefox-appdir = browser +skip-if = toolkit == 'android' +support-files = + Library/** + AppData/** + +[test_automigration.js] +[test_Chrome_cookies.js] +skip-if = os != "mac" # Relies on ULibDir +[test_Chrome_passwords.js] +skip-if = os != "win" +[test_Edge_availability.js] +[test_Edge_db_migration.js] +skip-if = os != "win" || os_version == "5.1" || os_version == "5.2" # Relies on post-XP bits of ESEDB +[test_fx_telemetry.js] +[test_IE_bookmarks.js] +skip-if = os != "win" +[test_IE_cookies.js] +skip-if = os != "win" +[test_IE7_passwords.js] +skip-if = os != "win" +[test_Safari_bookmarks.js] +skip-if = os != "mac" |