summaryrefslogtreecommitdiffstats
path: root/browser/components/migration/tests
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/migration/tests')
-rw-r--r--browser/components/migration/tests/chrome/chrome.ini4
-rw-r--r--browser/components/migration/tests/chrome/test_migration_wizard.html33
-rw-r--r--browser/components/migration/tests/marionette/manifest.ini4
-rw-r--r--browser/components/migration/tests/marionette/test_refresh_firefox.py688
-rw-r--r--browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Databin0 -> 24576 bytes
-rw-r--r--browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Local State5
-rw-r--r--browser/components/migration/tests/unit/AppData/LocalWithNoData/Google/Chrome/User Data/Default/Login Databin0 -> 24576 bytes
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/0f3ab103a522f4463ecacc36d34eb996/360sefav.datbin0 -> 6144 bytes
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/360Bookmarks1
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2020_08_28.favdb3
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2021_12_02.favdb3
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2020_08_28.favdb1
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2021_12_02.favdb1
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat1
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/DailyBackup/360sefav_2020_08_28.favdb3
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/Bookmarks3
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Local State12
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookiesbin0 -> 10240 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/_locales/en_US/messages.json9
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/manifest.json10
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/_locales/en_US/messages.json9
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/manifest.json5
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-2/1.0_0/manifest.json5
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryMasterbin0 -> 118784 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Login Databin0 -> 24576 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Local State22
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Bookmarks.plistbin0 -> 1956 bytes
-rw-r--r--browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Default/Login Databin0 -> 24576 bytes
-rw-r--r--browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Local State22
-rw-r--r--browser/components/migration/tests/unit/head_migration.js124
-rw-r--r--browser/components/migration/tests/unit/insertIEHistory/InsertIEHistory.cpp38
-rw-r--r--browser/components/migration/tests/unit/insertIEHistory/moz.build19
-rw-r--r--browser/components/migration/tests/unit/test_360seMigrationUtils.js169
-rw-r--r--browser/components/migration/tests/unit/test_360se_bookmarks.js62
-rw-r--r--browser/components/migration/tests/unit/test_ChromeMigrationUtils.js84
-rw-r--r--browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js113
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_bookmarks.js218
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_cookies.js72
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_history.js206
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_passwords.js379
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_passwords_emptySource.js83
-rw-r--r--browser/components/migration/tests/unit/test_Edge_db_migration.js773
-rw-r--r--browser/components/migration/tests/unit/test_IE7_passwords.js1371
-rw-r--r--browser/components/migration/tests/unit/test_IE_bookmarks.js30
-rw-r--r--browser/components/migration/tests/unit/test_IE_cookies.js149
-rw-r--r--browser/components/migration/tests/unit/test_IE_history.js50
-rw-r--r--browser/components/migration/tests/unit/test_MigrationUtils_timedRetry.js25
-rw-r--r--browser/components/migration/tests/unit/test_Safari_bookmarks.js69
-rw-r--r--browser/components/migration/tests/unit/test_fx_telemetry.js269
-rw-r--r--browser/components/migration/tests/unit/xpcshell.ini49
50 files changed, 5196 insertions, 0 deletions
diff --git a/browser/components/migration/tests/chrome/chrome.ini b/browser/components/migration/tests/chrome/chrome.ini
new file mode 100644
index 0000000000..2df25e8304
--- /dev/null
+++ b/browser/components/migration/tests/chrome/chrome.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+skip-if = os == 'android'
+
+[test_migration_wizard.html]
diff --git a/browser/components/migration/tests/chrome/test_migration_wizard.html b/browser/components/migration/tests/chrome/test_migration_wizard.html
new file mode 100644
index 0000000000..50e9300aa6
--- /dev/null
+++ b/browser/components/migration/tests/chrome/test_migration_wizard.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Basic tests for the Migration Wizard component</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://browser/content/migration/migration-wizard.mjs" type="module"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script>
+ "use strict";
+
+ /**
+ * Tests that the MigrationWizard:Init event is fired when the <migration-wizard>
+ * is added to the DOM.
+ */
+ add_task(async function test_init_event() {
+ let wiz = document.createElement("migration-wizard");
+ let content = document.getElementById("content");
+ let promise = new Promise(resolve => {
+ content.addEventListener("MigrationWizard:Init", resolve, { once: true });
+ })
+ content.appendChild(wiz);
+ await promise;
+ ok(true, "Saw MigrationWizard:Init event.");
+ });
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/browser/components/migration/tests/marionette/manifest.ini b/browser/components/migration/tests/marionette/manifest.ini
new file mode 100644
index 0000000000..afba7ead73
--- /dev/null
+++ b/browser/components/migration/tests/marionette/manifest.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+run-if = buildapp == 'browser'
+
+[test_refresh_firefox.py]
diff --git a/browser/components/migration/tests/marionette/test_refresh_firefox.py b/browser/components/migration/tests/marionette/test_refresh_firefox.py
new file mode 100644
index 0000000000..3ce4f52192
--- /dev/null
+++ b/browser/components/migration/tests/marionette/test_refresh_firefox.py
@@ -0,0 +1,688 @@
+import os
+import time
+
+from marionette_driver.errors import NoAlertPresentException
+from marionette_harness import MarionetteTestCase
+
+
+# Holds info about things we need to cleanup after the tests are done.
+class PendingCleanup:
+ desktop_backup_path = None
+ reset_profile_path = None
+ reset_profile_local_path = None
+
+ def __init__(self, profile_name_to_remove):
+ self.profile_name_to_remove = profile_name_to_remove
+
+
+class TestFirefoxRefresh(MarionetteTestCase):
+ _sandbox = "firefox-refresh"
+
+ _username = "marionette-test-login"
+ _password = "marionette-test-password"
+ _bookmarkURL = "about:mozilla"
+ _bookmarkText = "Some bookmark from Marionette"
+
+ _cookieHost = "firefox-refresh.marionette-test.mozilla.org"
+ _cookiePath = "some/cookie/path"
+ _cookieName = "somecookie"
+ _cookieValue = "some cookie value"
+
+ _historyURL = "http://firefox-refresh.marionette-test.mozilla.org/"
+ _historyTitle = "Test visit for Firefox Reset"
+
+ _formHistoryFieldName = "some-very-unique-marionette-only-firefox-reset-field"
+ _formHistoryValue = "special-pumpkin-value"
+
+ _formAutofillAvailable = False
+ _formAutofillAddressGuid = None
+
+ _expectedURLs = ["about:robots", "about:mozilla"]
+
+ def savePassword(self):
+ self.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 createBookmarkInMenu(self):
+ error = self.runAsyncCode(
+ """
+ // let url = arguments[0];
+ // let title = arguments[1];
+ // let resolve = arguments[arguments.length - 1];
+ let [url, title, resolve] = arguments;
+ PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid, url, title
+ }).then(() => resolve(false), resolve);
+ """,
+ script_args=(self._bookmarkURL, self._bookmarkText),
+ )
+ if error:
+ print(error)
+
+ def createBookmarksOnToolbar(self):
+ error = self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ let children = [];
+ for (let i = 1; i <= 5; i++) {
+ children.push({url: `about:rights?p=${i}`, title: `Bookmark ${i}`});
+ }
+ PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children
+ }).then(() => resolve(false), resolve);
+ """
+ )
+ if error:
+ print(error)
+
+ def createHistory(self):
+ error = self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ PlacesUtils.history.insert({
+ url: arguments[0],
+ title: arguments[1],
+ visits: [{
+ date: new Date(Date.now() - 5000),
+ referrer: "about:mozilla"
+ }]
+ }).then(() => resolve(false),
+ ex => resolve("Unexpected error in adding visit: " + ex));
+ """,
+ script_args=(self._historyURL, self._historyTitle),
+ )
+ if error:
+ print(error)
+
+ def createFormHistory(self):
+ error = self.runAsyncCode(
+ """
+ let updateDefinition = {
+ op: "add",
+ fieldname: arguments[0],
+ value: arguments[1],
+ firstUsed: (Date.now() - 5000) * 1000,
+ };
+ let resolve = arguments[arguments.length - 1];
+ global.FormHistory.update(updateDefinition).then(() => {
+ resolve(false);
+ }, error => {
+ resolve("Unexpected error in adding formhistory: " + error);
+ });
+ """,
+ script_args=(self._formHistoryFieldName, self._formHistoryValue),
+ )
+ if error:
+ print(error)
+
+ def createFormAutofill(self):
+ if not self._formAutofillAvailable:
+ return
+ self._formAutofillAddressGuid = self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ const TEST_ADDRESS_1 = {
+ "given-name": "John",
+ "additional-name": "R.",
+ "family-name": "Smith",
+ organization: "World Wide Web Consortium",
+ "street-address": "32 Vassar Street\\\nMIT Room 32-G524",
+ "address-level2": "Cambridge",
+ "address-level1": "MA",
+ "postal-code": "02139",
+ country: "US",
+ tel: "+15195555555",
+ email: "user@example.com",
+ };
+ return global.formAutofillStorage.initialize().then(() => {
+ return global.formAutofillStorage.addresses.add(TEST_ADDRESS_1);
+ }).then(resolve);
+ """
+ )
+
+ def createCookie(self):
+ self.runCode(
+ """
+ // Expire in 15 minutes:
+ let expireTime = Math.floor(Date.now() / 1000) + 15 * 60;
+ Services.cookies.add(arguments[0], arguments[1], arguments[2], arguments[3],
+ true, false, false, expireTime, {},
+ Ci.nsICookie.SAMESITE_NONE, Ci.nsICookie.SCHEME_UNSET);
+ """,
+ script_args=(
+ self._cookieHost,
+ self._cookiePath,
+ self._cookieName,
+ self._cookieValue,
+ ),
+ )
+
+ def createSession(self):
+ self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ const COMPLETE_STATE = Ci.nsIWebProgressListener.STATE_STOP +
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+ let { TabStateFlusher } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
+ );
+ let expectedURLs = Array.from(arguments[0])
+ gBrowser.addTabsProgressListener({
+ onStateChange(browser, webprogress, request, flags, status) {
+ try {
+ request && request.QueryInterface(Ci.nsIChannel);
+ } catch (ex) {}
+ let uriLoaded = request.originalURI && request.originalURI.spec;
+ if ((flags & COMPLETE_STATE == COMPLETE_STATE) && uriLoaded &&
+ expectedURLs.includes(uriLoaded)) {
+ TabStateFlusher.flush(browser).then(function() {
+ expectedURLs.splice(expectedURLs.indexOf(uriLoaded), 1);
+ if (!expectedURLs.length) {
+ gBrowser.removeTabsProgressListener(this);
+ resolve();
+ }
+ });
+ }
+ }
+ });
+ let expectedTabs = new Set();
+ for (let url of expectedURLs) {
+ expectedTabs.add(gBrowser.addTab(url, {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ }));
+ }
+ // Close any other tabs that might be open:
+ let allTabs = Array.from(gBrowser.tabs);
+ for (let tab of allTabs) {
+ if (!expectedTabs.has(tab)) {
+ gBrowser.removeTab(tab);
+ }
+ }
+ """, # NOQA: E501
+ script_args=(self._expectedURLs,),
+ )
+
+ def createFxa(self):
+ # This script will write an entry to the login manager and create
+ # a signedInUser.json in the profile dir.
+ self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ let { FxAccountsStorageManager } = ChromeUtils.import(
+ "resource://gre/modules/FxAccountsStorage.jsm"
+ );
+ let storage = new FxAccountsStorageManager();
+ let data = {email: "test@test.com", uid: "uid", keyFetchToken: "top-secret"};
+ storage.initialize(data);
+ storage.finalize().then(resolve);
+ """
+ )
+
+ def createSync(self):
+ # This script will write the canonical preference which indicates a user
+ # is signed into sync.
+ self.marionette.execute_script(
+ """
+ Services.prefs.setStringPref("services.sync.username", "test@test.com");
+ """
+ )
+
+ def checkPassword(self):
+ loginInfo = self.marionette.execute_script(
+ """
+ let ary = Services.logins.findLogins(
+ "test.marionette.mozilla.com",
+ "http://test.marionette.mozilla.com/some/form/",
+ null, {});
+ return ary.length ? ary : {username: "null", password: "null"};
+ """
+ )
+ self.assertEqual(len(loginInfo), 1)
+ self.assertEqual(loginInfo[0]["username"], self._username)
+ self.assertEqual(loginInfo[0]["password"], self._password)
+
+ loginCount = self.marionette.execute_script(
+ """
+ return Services.logins.getAllLogins().length;
+ """
+ )
+ # Note that we expect 2 logins - one from us, one from sync.
+ self.assertEqual(loginCount, 2, "No other logins are present")
+
+ def checkBookmarkInMenu(self):
+ titleInBookmarks = self.runAsyncCode(
+ """
+ let [url, resolve] = arguments;
+ PlacesUtils.bookmarks.fetch({url}).then(
+ bookmark => resolve(bookmark ? bookmark.title : ""),
+ ex => resolve(ex)
+ );
+ """,
+ script_args=(self._bookmarkURL,),
+ )
+ self.assertEqual(titleInBookmarks, self._bookmarkText)
+
+ def checkBookmarkToolbarVisibility(self):
+ toolbarVisible = self.marionette.execute_script(
+ """
+ const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL;
+ return Services.xulStore.getValue(BROWSER_DOCURL, "PersonalToolbar", "collapsed");
+ """
+ )
+ if toolbarVisible == "":
+ toolbarVisible = "false"
+ self.assertEqual(toolbarVisible, "false")
+
+ def checkHistory(self):
+ historyResult = self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ PlacesUtils.history.fetch(arguments[0]).then(pageInfo => {
+ if (!pageInfo) {
+ resolve("No visits found");
+ } else {
+ resolve(pageInfo);
+ }
+ }).catch(e => {
+ resolve("Unexpected error in fetching page: " + e);
+ });
+ """,
+ script_args=(self._historyURL,),
+ )
+ if type(historyResult) == str:
+ self.fail(historyResult)
+ return
+
+ self.assertEqual(historyResult["title"], self._historyTitle)
+
+ def checkFormHistory(self):
+ formFieldResults = self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ let results = [];
+ global.FormHistory.search(["value"], {fieldname: arguments[0]})
+ .then(resolve);
+ """,
+ script_args=(self._formHistoryFieldName,),
+ )
+ if type(formFieldResults) == str:
+ self.fail(formFieldResults)
+ return
+
+ formFieldResultCount = len(formFieldResults)
+ self.assertEqual(
+ formFieldResultCount,
+ 1,
+ "Should have exactly 1 entry for this field, got %d" % formFieldResultCount,
+ )
+ if formFieldResultCount == 1:
+ self.assertEqual(formFieldResults[0]["value"], self._formHistoryValue)
+
+ formHistoryCount = self.runAsyncCode(
+ """
+ let [resolve] = arguments;
+ global.FormHistory.count({}).then(resolve);
+ """
+ )
+ self.assertEqual(
+ formHistoryCount, 1, "There should be only 1 entry in the form history"
+ )
+
+ def checkFormAutofill(self):
+ if not self._formAutofillAvailable:
+ return
+
+ formAutofillResults = self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ return global.formAutofillStorage.initialize().then(() => {
+ return global.formAutofillStorage.addresses.getAll()
+ }).then(resolve);
+ """,
+ )
+ if type(formAutofillResults) == str:
+ self.fail(formAutofillResults)
+ return
+
+ formAutofillAddressCount = len(formAutofillResults)
+ self.assertEqual(
+ formAutofillAddressCount,
+ 1,
+ "Should have exactly 1 saved address, got %d" % formAutofillAddressCount,
+ )
+ if formAutofillAddressCount == 1:
+ self.assertEqual(
+ formAutofillResults[0]["guid"], self._formAutofillAddressGuid
+ )
+
+ def checkCookie(self):
+ cookieInfo = self.runCode(
+ """
+ try {
+ let cookies = Services.cookies.getCookiesFromHost(arguments[0], {});
+ let cookie = null;
+ for (let hostCookie of cookies) {
+ // getCookiesFromHost returns any cookie from the BASE host.
+ if (hostCookie.rawHost != arguments[0])
+ continue;
+ if (cookie != null) {
+ return "more than 1 cookie! That shouldn't happen!";
+ }
+ cookie = hostCookie;
+ }
+ return {path: cookie.path, name: cookie.name, value: cookie.value};
+ } catch (ex) {
+ return "got exception trying to fetch cookie: " + ex;
+ }
+ """,
+ script_args=(self._cookieHost,),
+ )
+ if not isinstance(cookieInfo, dict):
+ self.fail(cookieInfo)
+ return
+ self.assertEqual(cookieInfo["path"], self._cookiePath)
+ self.assertEqual(cookieInfo["value"], self._cookieValue)
+ self.assertEqual(cookieInfo["name"], self._cookieName)
+
+ def checkSession(self):
+ tabURIs = self.runCode(
+ """
+ return [... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec)
+ """
+ )
+ self.assertSequenceEqual(tabURIs, ["about:welcomeback"])
+
+ # Dismiss modal dialog if any. This is mainly to dismiss the check for
+ # default browser dialog if it shows up.
+ try:
+ alert = self.marionette.switch_to_alert()
+ alert.dismiss()
+ except NoAlertPresentException:
+ pass
+
+ tabURIs = self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1]
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ window.addEventListener("SSWindowStateReady", function() {
+ window.addEventListener("SSTabRestored", function() {
+ resolve(Array.from(gBrowser.browsers, b => b.currentURI?.spec));
+ }, { capture: false, once: true });
+ }, { capture: false, once: true });
+
+ let fs = function() {
+ if (content.document.readyState === "complete") {
+ content.document.getElementById("errorTryAgain").click();
+ } else {
+ content.window.addEventListener("load", function(event) {
+ content.document.getElementById("errorTryAgain").click();
+ }, { once: true });
+ }
+ };
+
+ Services.prefs.setBoolPref("security.allow_parent_unrestricted_js_loads", true);
+ mm.loadFrameScript("data:application/javascript,(" + fs.toString() + ")()", true);
+ Services.prefs.setBoolPref("security.allow_parent_unrestricted_js_loads", false);
+ """ # NOQA: E501
+ )
+ self.assertSequenceEqual(tabURIs, self._expectedURLs)
+
+ def checkFxA(self):
+ result = self.runAsyncCode(
+ """
+ let { FxAccountsStorageManager } = ChromeUtils.import(
+ "resource://gre/modules/FxAccountsStorage.jsm"
+ );
+ let resolve = arguments[arguments.length - 1];
+ let storage = new FxAccountsStorageManager();
+ let result = {};
+ storage.initialize();
+ storage.getAccountData().then(data => {
+ result.accountData = data;
+ return storage.finalize();
+ }).then(() => {
+ resolve(result);
+ }).catch(err => {
+ resolve(err.toString());
+ });
+ """
+ )
+ if type(result) != dict:
+ self.fail(result)
+ return
+ self.assertEqual(result["accountData"]["email"], "test@test.com")
+ self.assertEqual(result["accountData"]["uid"], "uid")
+ self.assertEqual(result["accountData"]["keyFetchToken"], "top-secret")
+
+ def checkSync(self, expect_sync_user):
+ pref_value = self.marionette.execute_script(
+ """
+ return Services.prefs.getStringPref("services.sync.username", null);
+ """
+ )
+ expected_value = "test@test.com" if expect_sync_user else None
+ self.assertEqual(pref_value, expected_value)
+
+ def checkProfile(self, has_migrated=False, expect_sync_user=True):
+ self.checkPassword()
+ self.checkBookmarkInMenu()
+ self.checkHistory()
+ self.checkFormHistory()
+ self.checkFormAutofill()
+ self.checkCookie()
+ self.checkFxA()
+ self.checkSync(expect_sync_user)
+ if has_migrated:
+ self.checkBookmarkToolbarVisibility()
+ self.checkSession()
+
+ def createProfileData(self):
+ self.savePassword()
+ self.createBookmarkInMenu()
+ self.createBookmarksOnToolbar()
+ self.createHistory()
+ self.createFormHistory()
+ self.createFormAutofill()
+ self.createCookie()
+ self.createSession()
+ self.createFxa()
+ self.createSync()
+
+ def setUpScriptData(self):
+ self.marionette.set_context(self.marionette.CONTEXT_CHROME)
+ self.runCode(
+ """
+ window.global = {};
+ global.LoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init");
+ global.profSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(Ci.nsIToolkitProfileService);
+ global.Preferences = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+ ).Preferences;
+ global.FormHistory = ChromeUtils.import(
+ "resource://gre/modules/FormHistory.jsm"
+ ).FormHistory;
+ """ # NOQA: E501
+ )
+ self._formAutofillAvailable = self.runCode(
+ """
+ try {
+ global.formAutofillStorage = ChromeUtils.import(
+ "resource://formautofill/FormAutofillStorage.jsm"
+ ).formAutofillStorage;
+ } catch(e) {
+ return false;
+ }
+ return true;
+ """ # NOQA: E501
+ )
+
+ def runCode(self, script, *args, **kwargs):
+ return self.marionette.execute_script(
+ script, new_sandbox=False, sandbox=self._sandbox, *args, **kwargs
+ )
+
+ def runAsyncCode(self, script, *args, **kwargs):
+ return self.marionette.execute_async_script(
+ script, new_sandbox=False, sandbox=self._sandbox, *args, **kwargs
+ )
+
+ def setUp(self):
+ MarionetteTestCase.setUp(self)
+ self.setUpScriptData()
+
+ self.cleanups = []
+
+ def tearDown(self):
+ # Force yet another restart with a clean profile to disconnect from the
+ # profile and environment changes we've made, to leave a more or less
+ # blank slate for the next person.
+ self.marionette.restart(in_app=False, clean=True)
+ self.setUpScriptData()
+
+ # Super
+ MarionetteTestCase.tearDown(self)
+
+ # A helper to deal with removing a load of files
+ import mozfile
+
+ for cleanup in self.cleanups:
+ if cleanup.desktop_backup_path:
+ mozfile.remove(cleanup.desktop_backup_path)
+
+ if cleanup.reset_profile_path:
+ # Remove ourselves from profiles.ini
+ self.runCode(
+ """
+ let name = arguments[0];
+ let profile = global.profSvc.getProfileByName(name);
+ profile.remove(false)
+ global.profSvc.flush();
+ """,
+ script_args=(cleanup.profile_name_to_remove,),
+ )
+ # Remove the local profile dir if it's not the same as the profile dir:
+ different_path = (
+ cleanup.reset_profile_local_path != cleanup.reset_profile_path
+ )
+ if cleanup.reset_profile_local_path and different_path:
+ mozfile.remove(cleanup.reset_profile_local_path)
+
+ # And delete all the files.
+ mozfile.remove(cleanup.reset_profile_path)
+
+ def doReset(self):
+ profileName = "marionette-test-profile-" + str(int(time.time() * 1000))
+ cleanup = PendingCleanup(profileName)
+ self.runCode(
+ """
+ // Ensure the current (temporary) profile is in profiles.ini:
+ let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let profileName = arguments[1];
+ let myProfile = global.profSvc.createProfile(profD, profileName);
+ global.profSvc.flush()
+
+ // Now add the reset parameters:
+ let prefsToKeep = Array.from(Services.prefs.getChildList("marionette."));
+ // Add all the modified preferences set from geckoinstance.py to avoid
+ // non-local connections.
+ prefsToKeep = prefsToKeep.concat(JSON.parse(
+ Services.env.get("MOZ_MARIONETTE_REQUIRED_PREFS")));
+ let prefObj = {};
+ for (let pref of prefsToKeep) {
+ prefObj[pref] = global.Preferences.get(pref);
+ }
+ Services.env.set("MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS", JSON.stringify(prefObj));
+ Services.env.set("MOZ_RESET_PROFILE_RESTART", "1");
+ Services.env.set("XRE_PROFILE_PATH", arguments[0]);
+ """,
+ script_args=(
+ self.marionette.instance.profile.profile,
+ profileName,
+ ),
+ )
+
+ profileLeafName = os.path.basename(
+ os.path.normpath(self.marionette.instance.profile.profile)
+ )
+
+ # Now restart the browser to get it reset:
+ self.marionette.restart(clean=False, in_app=True)
+ self.setUpScriptData()
+
+ # Determine the new profile path (we'll need to remove it when we're done)
+ [cleanup.reset_profile_path, cleanup.reset_profile_local_path] = self.runCode(
+ """
+ let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let localD = Services.dirsvc.get("ProfLD", Ci.nsIFile);
+ return [profD.path, localD.path];
+ """
+ )
+
+ # Determine the backup path
+ cleanup.desktop_backup_path = self.runCode(
+ """
+ let container;
+ try {
+ container = Services.dirsvc.get("Desk", Ci.nsIFile);
+ } catch (ex) {
+ container = Services.dirsvc.get("Home", Ci.nsIFile);
+ }
+ let bundle = Services.strings.createBundle("chrome://mozapps/locale/profile/profileSelection.properties");
+ let dirName = bundle.formatStringFromName("resetBackupDirectory", [Services.appinfo.name]);
+ container.append(dirName);
+ container.append(arguments[0]);
+ return container.path;
+ """, # NOQA: E501
+ script_args=(profileLeafName,),
+ )
+
+ self.assertTrue(
+ os.path.isdir(cleanup.reset_profile_path),
+ "Reset profile path should be present",
+ )
+ self.assertTrue(
+ os.path.isdir(cleanup.desktop_backup_path),
+ "Backup profile path should be present",
+ )
+ self.assertIn(cleanup.profile_name_to_remove, cleanup.reset_profile_path)
+ return cleanup
+
+ def testResetEverything(self):
+ self.createProfileData()
+
+ self.checkProfile(expect_sync_user=True)
+
+ this_cleanup = self.doReset()
+ self.cleanups.append(this_cleanup)
+
+ # Now check that we're doing OK...
+ self.checkProfile(has_migrated=True, expect_sync_user=True)
+
+ def testFxANoSync(self):
+ # This test doesn't need to repeat all the non-sync tests...
+ # Setup FxA but *not* sync
+ self.createFxa()
+
+ self.checkFxA()
+ self.checkSync(False)
+
+ this_cleanup = self.doReset()
+ self.cleanups.append(this_cleanup)
+
+ self.checkFxA()
+ self.checkSync(False)
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
new file mode 100644
index 0000000000..7e6e843a03
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data
Binary files differ
diff --git a/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Local State b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Local State
new file mode 100644
index 0000000000..3f3fecb651
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Local State
@@ -0,0 +1,5 @@
+{
+ "os_crypt" : {
+ "encrypted_key" : "RFBBUEk/ThisNPAPIKeyCanOnlyBeDecryptedByTheOriginalDeviceSoThisWillThrowFromDecryptData"
+ }
+}
diff --git a/browser/components/migration/tests/unit/AppData/LocalWithNoData/Google/Chrome/User Data/Default/Login Data b/browser/components/migration/tests/unit/AppData/LocalWithNoData/Google/Chrome/User Data/Default/Login Data
new file mode 100644
index 0000000000..fd135624c4
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/LocalWithNoData/Google/Chrome/User Data/Default/Login Data
Binary files differ
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat
new file mode 100644
index 0000000000..1835c33583
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat
Binary files differ
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/360Bookmarks b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/360Bookmarks
new file mode 100644
index 0000000000..f51195f54c
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/360Bookmarks
@@ -0,0 +1 @@
+Encrypted canonical bookmarks storage, since 360 SE 10
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2020_08_28.favdb b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2020_08_28.favdb
new file mode 100644
index 0000000000..ea466a25bf
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2020_08_28.favdb
@@ -0,0 +1,3 @@
+{
+ "note": "Plain text bookmarks backup, since 360 SE 10."
+}
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2021_12_02.favdb b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2021_12_02.favdb
new file mode 100644
index 0000000000..ea466a25bf
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2021_12_02.favdb
@@ -0,0 +1,3 @@
+{
+ "note": "Plain text bookmarks backup, since 360 SE 10."
+}
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2020_08_28.favdb b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2020_08_28.favdb
new file mode 100644
index 0000000000..32b4002a32
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2020_08_28.favdb
@@ -0,0 +1 @@
+Encrypted bookmarks backup, since 360 SE 10.
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2021_12_02.favdb b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2021_12_02.favdb
new file mode 100644
index 0000000000..32b4002a32
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2021_12_02.favdb
@@ -0,0 +1 @@
+Encrypted bookmarks backup, since 360 SE 10.
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat
new file mode 100644
index 0000000000..440e7145bd
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat
@@ -0,0 +1 @@
+Bookmarks storage in legacy SQLite format.
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/DailyBackup/360sefav_2020_08_28.favdb b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/DailyBackup/360sefav_2020_08_28.favdb
new file mode 100644
index 0000000000..d5d939629c
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/DailyBackup/360sefav_2020_08_28.favdb
@@ -0,0 +1,3 @@
+{
+ "note": "Plain text bookmarks backup, 360 SE 9."
+}
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/Bookmarks b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/Bookmarks
new file mode 100644
index 0000000000..6f47e5a55c
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/Bookmarks
@@ -0,0 +1,3 @@
+{
+ "note": "Plain text canonical bookmarks storage, 360 SE 9."
+}
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Local State b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Local State
new file mode 100644
index 0000000000..dd3fecce45
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Local State
@@ -0,0 +1,12 @@
+{
+ "profile": {
+ "info_cache": {
+ "Default": {
+ "name": "用户1"
+ }
+ }
+ },
+ "sync_login_info": {
+ "filepath": "0f3ab103a522f4463ecacc36d34eb996"
+ }
+}
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
new file mode 100644
index 0000000000..83d855cb33
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookies
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/_locales/en_US/messages.json b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/_locales/en_US/messages.json
new file mode 100644
index 0000000000..44e855edbd
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/_locales/en_US/messages.json
@@ -0,0 +1,9 @@
+{
+ "description": {
+ "description": "Extension description in manifest. Should not exceed 132 characters.",
+ "message": "It is the description of fake app 1."
+ },
+ "name": {
+ "message": "Fake App 1"
+ }
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/manifest.json b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/manifest.json
new file mode 100644
index 0000000000..1550bf1c0e
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/manifest.json
@@ -0,0 +1,10 @@
+{
+ "app": {
+ "launch": {
+ "local_path": "main.html"
+ }
+ },
+ "default_locale": "en_US",
+ "description": "__MSG_description__",
+ "name": "__MSG_name__"
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/_locales/en_US/messages.json b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/_locales/en_US/messages.json
new file mode 100644
index 0000000000..11657460d8
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/_locales/en_US/messages.json
@@ -0,0 +1,9 @@
+{
+ "description": {
+ "description": "Extension description in manifest. Should not exceed 132 characters.",
+ "message": "It is the description of fake extension 1."
+ },
+ "name": {
+ "message": "Fake Extension 1"
+ }
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/manifest.json b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/manifest.json
new file mode 100644
index 0000000000..5ceced8031
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/manifest.json
@@ -0,0 +1,5 @@
+{
+ "default_locale": "en_US",
+ "description": "__MSG_description__",
+ "name": "__MSG_name__"
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-2/1.0_0/manifest.json b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-2/1.0_0/manifest.json
new file mode 100644
index 0000000000..0333a91e56
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-2/1.0_0/manifest.json
@@ -0,0 +1,5 @@
+{
+ "default_locale": "en_US",
+ "description": "It is the description of fake extension 2.",
+ "name": "Fake Extension 2"
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryMaster b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryMaster
new file mode 100644
index 0000000000..7fb19903b0
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryMaster
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Login Data b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Login Data
new file mode 100644
index 0000000000..19b8542b98
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Login Data
Binary files differ
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 0000000000..01b99455e4
--- /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
new file mode 100644
index 0000000000..8046d5e9c9
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Bookmarks.plist
Binary files differ
diff --git a/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Default/Login Data b/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Default/Login Data
new file mode 100644
index 0000000000..b2d425eb4a
--- /dev/null
+++ b/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Default/Login Data
Binary files differ
diff --git a/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Local State b/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Local State
new file mode 100644
index 0000000000..bb03d6b9a1
--- /dev/null
+++ b/browser/components/migration/tests/unit/LibraryWithNoData/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 With No Data"
+ }
+ },
+ "profiles_created" : 1,
+ "last_used" : "Default",
+ "last_active_profiles" : [
+ "Default"
+ ]
+ }
+}
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 0000000000..ab1f34e419
--- /dev/null
+++ b/browser/components/migration/tests/unit/head_migration.js
@@ -0,0 +1,124 @@
+"use strict";
+
+var { MigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/MigrationUtils.sys.mjs"
+);
+var { LoginHelper } = ChromeUtils.import(
+ "resource://gre/modules/LoginHelper.jsm"
+);
+var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+var { PlacesUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesUtils.sys.mjs"
+);
+var { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+var { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+var { PlacesTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PlacesTestUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
+});
+
+// Initialize profile.
+var gProfD = do_get_profile();
+
+var { getAppInfo, newAppInfo, updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+updateAppInfo();
+
+/**
+ * Migrates the requested resource and waits for the migration to be complete.
+ *
+ * @param {MigratorBase} migrator
+ * The migrator being used to migrate the data.
+ * @param {number} resourceType
+ * This is a bitfield with bits from nsIBrowserProfileMigrator flipped to indicate what
+ * resources should be migrated.
+ * @param {object|string|null} [aProfile=null]
+ * The profile to be migrated. If set to null, the default profile will be
+ * migrated.
+ * @param {boolean} succeeds
+ * True if this migration is expected to succeed.
+ * @returns {Promise<Array<string[]>>}
+ * An array of the results from each nsIObserver topics being observed to
+ * verify if the migration succeeded or failed. Those results are 2-element
+ * arrays of [subject, data].
+ */
+async function promiseMigration(
+ migrator,
+ resourceType,
+ aProfile = null,
+ succeeds = null
+) {
+ // Ensure resource migration is available.
+ let availableSources = await migrator.getMigrateData(aProfile);
+ Assert.ok(
+ (availableSources & resourceType) > 0,
+ "Resource supported by migrator"
+ );
+ let promises = [TestUtils.topicObserved("Migration:Ended")];
+
+ if (succeeds !== null) {
+ // Check that the specific resource type succeeded
+ promises.push(
+ TestUtils.topicObserved(
+ succeeds ? "Migration:ItemAfterMigrate" : "Migration:ItemError",
+ (_, data) => data == resourceType
+ )
+ );
+ }
+
+ // Start the migration.
+ migrator.migrate(resourceType, null, aProfile);
+
+ return Promise.all(promises);
+}
+
+/**
+ * Replaces a directory service entry with a given nsIFile.
+ *
+ * @param {string} key
+ * The nsIDirectoryService directory key to register a fake path for.
+ * For example: "AppData", "ULibDir".
+ * @param {nsIFile} file
+ * The nsIFile to map the key to. Note that this nsIFile should represent
+ * a directory and not an individual file.
+ * @see nsDirectoryServiceDefs.h for the list of directories that can be
+ * overridden.
+ */
+function registerFakePath(key, file) {
+ let dirsvc = Services.dirsvc.QueryInterface(Ci.nsIProperties);
+ let originalFile;
+ try {
+ // If a file is already provided save it and undefine, otherwise set will
+ // throw for persistent entries (ones that are cached).
+ originalFile = dirsvc.get(key, Ci.nsIFile);
+ dirsvc.undefine(key);
+ } catch (e) {
+ // dirsvc.get will throw if nothing provides for the key and dirsvc.undefine
+ // will throw if it's not a persistent entry, in either case we don't want
+ // to set the original file in cleanup.
+ originalFile = undefined;
+ }
+
+ dirsvc.set(key, file);
+ registerCleanupFunction(() => {
+ dirsvc.undefine(key);
+ if (originalFile) {
+ dirsvc.set(key, originalFile);
+ }
+ });
+}
diff --git a/browser/components/migration/tests/unit/insertIEHistory/InsertIEHistory.cpp b/browser/components/migration/tests/unit/insertIEHistory/InsertIEHistory.cpp
new file mode 100644
index 0000000000..cdc1faff7d
--- /dev/null
+++ b/browser/components/migration/tests/unit/insertIEHistory/InsertIEHistory.cpp
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Insert URLs into Internet Explorer (IE) history so we can test importing
+ * them.
+ *
+ * See API docs at
+ * https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/ms774949(v%3dvs.85)
+ */
+
+#include <urlhist.h> // IUrlHistoryStg
+#include <shlguid.h> // SID_SUrlHistory
+
+int main(int argc, char** argv) {
+ HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
+ if (FAILED(hr)) {
+ CoUninitialize();
+ return -1;
+ }
+ IUrlHistoryStg* ieHist;
+
+ hr =
+ ::CoCreateInstance(CLSID_CUrlHistory, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IUrlHistoryStg, reinterpret_cast<void**>(&ieHist));
+ if (FAILED(hr)) return -2;
+
+ hr = ieHist->AddUrl(L"http://www.mozilla.org/1", L"Mozilla HTTP Test", 0);
+ if (FAILED(hr)) return -3;
+
+ hr = ieHist->AddUrl(L"https://www.mozilla.org/2", L"Mozilla HTTPS Test 🦊",
+ 0);
+ if (FAILED(hr)) return -4;
+
+ CoUninitialize();
+
+ return 0;
+}
diff --git a/browser/components/migration/tests/unit/insertIEHistory/moz.build b/browser/components/migration/tests/unit/insertIEHistory/moz.build
new file mode 100644
index 0000000000..61ca96d48a
--- /dev/null
+++ b/browser/components/migration/tests/unit/insertIEHistory/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+FINAL_TARGET = "_tests/xpcshell/browser/components/migration/tests/unit"
+
+Program("InsertIEHistory")
+OS_LIBS += [
+ "ole32",
+ "uuid",
+]
+SOURCES += [
+ "InsertIEHistory.cpp",
+]
+
+NO_PGO = True
+DisableStlWrapping()
diff --git a/browser/components/migration/tests/unit/test_360seMigrationUtils.js b/browser/components/migration/tests/unit/test_360seMigrationUtils.js
new file mode 100644
index 0000000000..9dd7704317
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_360seMigrationUtils.js
@@ -0,0 +1,169 @@
+"use strict";
+
+const { Qihoo360seMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/360seMigrationUtils.sys.mjs"
+);
+
+const parentPath = do_get_file("AppData/Roaming/360se6/User Data").path;
+const loggedInPath = "0f3ab103a522f4463ecacc36d34eb996";
+const loggedInBackup = PathUtils.join(
+ parentPath,
+ "Default",
+ loggedInPath,
+ "DailyBackup",
+ "360default_ori_2021_12_02.favdb"
+);
+const loggedOutBackup = PathUtils.join(
+ parentPath,
+ "Default",
+ "DailyBackup",
+ "360default_ori_2021_12_02.favdb"
+);
+
+function getSqlitePath(profileId) {
+ return PathUtils.join(parentPath, profileId, loggedInPath, "360sefav.dat");
+}
+
+add_task(async function test_360se10_logged_in() {
+ const sqlitePath = getSqlitePath("Default");
+ await IOUtils.setModificationTime(sqlitePath);
+ await IOUtils.copy(
+ PathUtils.join(parentPath, "Default", "360Bookmarks"),
+ PathUtils.join(parentPath, "Default", loggedInPath)
+ );
+ await IOUtils.copy(loggedOutBackup, loggedInBackup);
+
+ const alternativeBookmarks = await Qihoo360seMigrationUtils.getAlternativeBookmarks(
+ {
+ bookmarksPath: PathUtils.join(parentPath, "Default", "Bookmarks"),
+ localState: {
+ sync_login_info: {
+ filepath: loggedInPath,
+ },
+ },
+ }
+ );
+ Assert.ok(
+ alternativeBookmarks.resource && alternativeBookmarks.resource.exists,
+ "Should return the legacy bookmark resource."
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.path,
+ undefined,
+ "Should not return any path to plain text bookmarks."
+ );
+});
+
+add_task(async function test_360se10_logged_in_outdated_sqlite() {
+ const sqlitePath = getSqlitePath("Default");
+ await IOUtils.setModificationTime(
+ sqlitePath,
+ new Date("2020-08-18").valueOf()
+ );
+
+ const alternativeBookmarks = await Qihoo360seMigrationUtils.getAlternativeBookmarks(
+ {
+ bookmarksPath: PathUtils.join(parentPath, "Default", "Bookmarks"),
+ localState: {
+ sync_login_info: {
+ filepath: loggedInPath,
+ },
+ },
+ }
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.resource,
+ undefined,
+ "Should not return the outdated legacy bookmark resource."
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.path,
+ loggedInBackup,
+ "Should return path to the most recent plain text bookmarks backup."
+ );
+
+ await IOUtils.setModificationTime(sqlitePath);
+});
+
+add_task(async function test_360se10_logged_out() {
+ const alternativeBookmarks = await Qihoo360seMigrationUtils.getAlternativeBookmarks(
+ {
+ bookmarksPath: PathUtils.join(parentPath, "Default", "Bookmarks"),
+ localState: {
+ sync_login_info: {
+ filepath: "",
+ },
+ },
+ }
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.resource,
+ undefined,
+ "Should not return the legacy bookmark resource."
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.path,
+ loggedOutBackup,
+ "Should return path to the most recent plain text bookmarks backup."
+ );
+});
+
+add_task(async function test_360se9_logged_in_outdated_sqlite() {
+ const sqlitePath = getSqlitePath("Default4SE9Test");
+ await IOUtils.setModificationTime(
+ sqlitePath,
+ new Date("2020-08-18").valueOf()
+ );
+
+ const alternativeBookmarks = await Qihoo360seMigrationUtils.getAlternativeBookmarks(
+ {
+ bookmarksPath: PathUtils.join(parentPath, "Default4SE9Test", "Bookmarks"),
+ localState: {
+ sync_login_info: {
+ filepath: loggedInPath,
+ },
+ },
+ }
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.resource,
+ undefined,
+ "Should not return the legacy bookmark resource."
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.path,
+ PathUtils.join(
+ parentPath,
+ "Default4SE9Test",
+ loggedInPath,
+ "DailyBackup",
+ "360sefav_2020_08_28.favdb"
+ ),
+ "Should return path to the most recent plain text bookmarks backup."
+ );
+
+ await IOUtils.setModificationTime(sqlitePath);
+});
+
+add_task(async function test_360se9_logged_out() {
+ const alternativeBookmarks = await Qihoo360seMigrationUtils.getAlternativeBookmarks(
+ {
+ bookmarksPath: PathUtils.join(parentPath, "Default4SE9Test", "Bookmarks"),
+ localState: {
+ sync_login_info: {
+ filepath: "",
+ },
+ },
+ }
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.resource,
+ undefined,
+ "Should not return the legacy bookmark resource."
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.path,
+ PathUtils.join(parentPath, "Default4SE9Test", "Bookmarks"),
+ "Should return path to the plain text canonical bookmarks."
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_360se_bookmarks.js b/browser/components/migration/tests/unit/test_360se_bookmarks.js
new file mode 100644
index 0000000000..e976aac609
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_360se_bookmarks.js
@@ -0,0 +1,62 @@
+"use strict";
+
+const { CustomizableUI } = ChromeUtils.import(
+ "resource:///modules/CustomizableUI.jsm"
+);
+
+add_task(async function() {
+ registerFakePath("AppData", do_get_file("AppData/Roaming/"));
+
+ let migrator = await MigrationUtils.getMigrator("chromium-360se");
+ // Sanity check for the source.
+ Assert.ok(await migrator.isSourceAvailable());
+
+ let importedToBookmarksToolbar = false;
+ let itemsSeen = { bookmarks: 0, folders: 0 };
+
+ let listener = events => {
+ for (let event of events) {
+ itemsSeen[
+ event.itemType == PlacesUtils.bookmarks.TYPE_FOLDER
+ ? "folders"
+ : "bookmarks"
+ ]++;
+ if (event.parentGuid == PlacesUtils.bookmarks.toolbarGuid) {
+ importedToBookmarksToolbar = true;
+ }
+ }
+ };
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+ let observerNotified = false;
+ Services.obs.addObserver((aSubject, aTopic, aData) => {
+ let [toolbar, visibility] = JSON.parse(aData);
+ Assert.equal(
+ toolbar,
+ CustomizableUI.AREA_BOOKMARKS,
+ "Notification should be received for bookmarks toolbar"
+ );
+ Assert.equal(
+ visibility,
+ "true",
+ "Notification should say to reveal the bookmarks toolbar"
+ );
+ observerNotified = true;
+ }, "browser-set-toolbar-visibility");
+
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS, {
+ id: "Default",
+ });
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+
+ // Check the bookmarks have been imported to all the expected parents.
+ Assert.ok(importedToBookmarksToolbar, "Bookmarks imported in the toolbar");
+ Assert.equal(itemsSeen.bookmarks, 8, "Should import all bookmarks.");
+ Assert.equal(itemsSeen.folders, 2, "Should import all folders.");
+ // Check that the telemetry matches:
+ Assert.equal(
+ MigrationUtils._importQuantities.bookmarks,
+ itemsSeen.bookmarks + itemsSeen.folders,
+ "Telemetry reporting correct."
+ );
+ Assert.ok(observerNotified, "The observer should be notified upon migration");
+});
diff --git a/browser/components/migration/tests/unit/test_ChromeMigrationUtils.js b/browser/components/migration/tests/unit/test_ChromeMigrationUtils.js
new file mode 100644
index 0000000000..c82c959333
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_ChromeMigrationUtils.js
@@ -0,0 +1,84 @@
+"use strict";
+
+const { ChromeMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMigrationUtils.sys.mjs"
+);
+
+// Setup chrome user data path for all platforms.
+ChromeMigrationUtils.getDataPath = () => {
+ return do_get_file("Library/Application Support/Google/Chrome/").path;
+};
+
+add_task(async function test_getExtensionList_function() {
+ let extensionList = await ChromeMigrationUtils.getExtensionList("Default");
+ Assert.equal(
+ extensionList.length,
+ 2,
+ "There should be 2 extensions installed."
+ );
+ Assert.deepEqual(
+ extensionList.find(extension => extension.id == "fake-extension-1"),
+ {
+ id: "fake-extension-1",
+ name: "Fake Extension 1",
+ description: "It is the description of fake extension 1.",
+ },
+ "First extension should match expectations."
+ );
+ Assert.deepEqual(
+ extensionList.find(extension => extension.id == "fake-extension-2"),
+ {
+ id: "fake-extension-2",
+ name: "Fake Extension 2",
+ description: "It is the description of fake extension 2.",
+ },
+ "Second extension should match expectations."
+ );
+});
+
+add_task(async function test_getExtensionInformation_function() {
+ let extension = await ChromeMigrationUtils.getExtensionInformation(
+ "fake-extension-1",
+ "Default"
+ );
+ Assert.deepEqual(
+ extension,
+ {
+ id: "fake-extension-1",
+ name: "Fake Extension 1",
+ description: "It is the description of fake extension 1.",
+ },
+ "Should get the extension information correctly."
+ );
+});
+
+add_task(async function test_getLocaleString_function() {
+ let name = await ChromeMigrationUtils._getLocaleString(
+ "__MSG_name__",
+ "en_US",
+ "fake-extension-1",
+ "Default"
+ );
+ Assert.deepEqual(
+ name,
+ "Fake Extension 1",
+ "The value of __MSG_name__ locale key is Fake Extension 1."
+ );
+});
+
+add_task(async function test_isExtensionInstalled_function() {
+ let isInstalled = await ChromeMigrationUtils.isExtensionInstalled(
+ "fake-extension-1",
+ "Default"
+ );
+ Assert.ok(isInstalled, "The fake-extension-1 extension should be installed.");
+});
+
+add_task(async function test_getLastUsedProfileId_function() {
+ let profileId = await ChromeMigrationUtils.getLastUsedProfileId();
+ Assert.equal(
+ profileId,
+ "Default",
+ "The last used profile ID should be Default."
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js b/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js
new file mode 100644
index 0000000000..f00402d95a
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js
@@ -0,0 +1,113 @@
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { ChromeMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMigrationUtils.sys.mjs"
+);
+
+function getRootPath() {
+ let dirKey;
+ if (AppConstants.platform == "win") {
+ dirKey = "LocalAppData";
+ } else if (AppConstants.platform == "macosx") {
+ dirKey = "ULibDir";
+ } else {
+ dirKey = "Home";
+ }
+ return Services.dirsvc.get(dirKey, Ci.nsIFile).path;
+}
+
+add_task(async function test_getDataPath_function() {
+ let chromeUserDataPath = ChromeMigrationUtils.getDataPath("Chrome");
+ let chromiumUserDataPath = ChromeMigrationUtils.getDataPath("Chromium");
+ let canaryUserDataPath = ChromeMigrationUtils.getDataPath("Canary");
+ if (AppConstants.platform == "win") {
+ Assert.equal(
+ chromeUserDataPath,
+ PathUtils.join(getRootPath(), "Google", "Chrome", "User Data"),
+ "Should get the path of Chrome data directory."
+ );
+ Assert.equal(
+ chromiumUserDataPath,
+ PathUtils.join(getRootPath(), "Chromium", "User Data"),
+ "Should get the path of Chromium data directory."
+ );
+ Assert.equal(
+ canaryUserDataPath,
+ PathUtils.join(getRootPath(), "Google", "Chrome SxS", "User Data"),
+ "Should get the path of Canary data directory."
+ );
+ } else if (AppConstants.platform == "macosx") {
+ Assert.equal(
+ chromeUserDataPath,
+ PathUtils.join(getRootPath(), "Application Support", "Google", "Chrome"),
+ "Should get the path of Chrome data directory."
+ );
+ Assert.equal(
+ chromiumUserDataPath,
+ PathUtils.join(getRootPath(), "Application Support", "Chromium"),
+ "Should get the path of Chromium data directory."
+ );
+ Assert.equal(
+ canaryUserDataPath,
+ PathUtils.join(
+ getRootPath(),
+ "Application Support",
+ "Google",
+ "Chrome Canary"
+ ),
+ "Should get the path of Canary data directory."
+ );
+ } else {
+ Assert.equal(
+ chromeUserDataPath,
+ PathUtils.join(getRootPath(), ".config", "google-chrome"),
+ "Should get the path of Chrome data directory."
+ );
+ Assert.equal(
+ chromiumUserDataPath,
+ PathUtils.join(getRootPath(), ".config", "chromium"),
+ "Should get the path of Chromium data directory."
+ );
+ Assert.equal(canaryUserDataPath, null, "Should get null for Canary.");
+ }
+});
+
+add_task(async function test_getExtensionPath_function() {
+ let extensionPath = ChromeMigrationUtils.getExtensionPath("Default");
+ let expectedPath;
+ if (AppConstants.platform == "win") {
+ expectedPath = PathUtils.join(
+ getRootPath(),
+ "Google",
+ "Chrome",
+ "User Data",
+ "Default",
+ "Extensions"
+ );
+ } else if (AppConstants.platform == "macosx") {
+ expectedPath = PathUtils.join(
+ getRootPath(),
+ "Application Support",
+ "Google",
+ "Chrome",
+ "Default",
+ "Extensions"
+ );
+ } else {
+ expectedPath = PathUtils.join(
+ getRootPath(),
+ ".config",
+ "google-chrome",
+ "Default",
+ "Extensions"
+ );
+ }
+ Assert.equal(
+ extensionPath,
+ expectedPath,
+ "Should get the path of extensions directory."
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_Chrome_bookmarks.js b/browser/components/migration/tests/unit/test_Chrome_bookmarks.js
new file mode 100644
index 0000000000..de1535cdab
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_bookmarks.js
@@ -0,0 +1,218 @@
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { CustomizableUI } = ChromeUtils.import(
+ "resource:///modules/CustomizableUI.jsm"
+);
+
+const { PlacesUIUtils } = ChromeUtils.importESModule(
+ "resource:///modules/PlacesUIUtils.sys.mjs"
+);
+
+let rootDir = do_get_file("chromefiles/", true);
+
+add_task(async function setup_fakePaths() {
+ let pathId;
+ if (AppConstants.platform == "macosx") {
+ pathId = "ULibDir";
+ } else if (AppConstants.platform == "win") {
+ pathId = "LocalAppData";
+ } else {
+ pathId = "Home";
+ }
+ registerFakePath(pathId, rootDir);
+});
+
+add_task(async function setup_initialBookmarks() {
+ let bookmarks = [];
+ for (let i = 0; i < PlacesUIUtils.NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE + 1; i++) {
+ bookmarks.push({ url: "https://example.com/" + i, title: "" + i });
+ }
+
+ // Ensure we have enough items in both the menu and toolbar to trip creating a "from" folder.
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: bookmarks,
+ });
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: bookmarks,
+ });
+});
+
+async function testBookmarks(migratorKey, subDirs) {
+ if (AppConstants.platform == "macosx") {
+ subDirs.unshift("Application Support");
+ } else if (AppConstants.platform == "win") {
+ subDirs.push("User Data");
+ } else {
+ subDirs.unshift(".config");
+ }
+
+ let target = rootDir.clone();
+ // Pretend this is the default profile
+ subDirs.push("Default");
+ while (subDirs.length) {
+ target.append(subDirs.shift());
+ }
+
+ await IOUtils.makeDirectory(target.path, {
+ createAncestor: true,
+ ignoreExisting: true,
+ });
+
+ target.append("Bookmarks");
+ await IOUtils.remove(target.path, { ignoreAbsent: true });
+
+ let bookmarksData = {
+ roots: { bookmark_bar: { children: [] }, other: { children: [] } },
+ };
+ const MAX_BMS = 100;
+ let barKids = bookmarksData.roots.bookmark_bar.children;
+ let menuKids = bookmarksData.roots.other.children;
+ let currentMenuKids = menuKids;
+ let currentBarKids = barKids;
+ for (let i = 0; i < MAX_BMS; i++) {
+ currentBarKids.push({
+ url: "https://www.chrome-bookmark-bar-bookmark" + i + ".com",
+ name: "bookmark " + i,
+ type: "url",
+ });
+ currentMenuKids.push({
+ url: "https://www.chrome-menu-bookmark" + i + ".com",
+ name: "bookmark for menu " + i,
+ type: "url",
+ });
+ if (i % 20 == 19) {
+ let nextFolder = {
+ name: "toolbar folder " + Math.ceil(i / 20),
+ type: "folder",
+ children: [],
+ };
+ currentBarKids.push(nextFolder);
+ currentBarKids = nextFolder.children;
+
+ nextFolder = {
+ name: "menu folder " + Math.ceil(i / 20),
+ type: "folder",
+ children: [],
+ };
+ currentMenuKids.push(nextFolder);
+ currentMenuKids = nextFolder.children;
+ }
+ }
+
+ await IOUtils.writeJSON(target.path, bookmarksData);
+
+ let migrator = await MigrationUtils.getMigrator(migratorKey);
+ // Sanity check for the source.
+ Assert.ok(await migrator.isSourceAvailable());
+
+ let itemsSeen = { bookmarks: 0, folders: 0 };
+ let listener = events => {
+ for (let event of events) {
+ itemsSeen[
+ event.itemType == PlacesUtils.bookmarks.TYPE_FOLDER
+ ? "folders"
+ : "bookmarks"
+ ]++;
+ }
+ };
+
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+ const PROFILE = {
+ id: "Default",
+ name: "Default",
+ };
+ let observerNotified = false;
+ Services.obs.addObserver((aSubject, aTopic, aData) => {
+ let [toolbar, visibility] = JSON.parse(aData);
+ Assert.equal(
+ toolbar,
+ CustomizableUI.AREA_BOOKMARKS,
+ "Notification should be received for bookmarks toolbar"
+ );
+ Assert.equal(
+ visibility,
+ "true",
+ "Notification should say to reveal the bookmarks toolbar"
+ );
+ observerNotified = true;
+ }, "browser-set-toolbar-visibility");
+ const initialToolbarCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.toolbarGuid
+ );
+ const initialUnfiledCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.unfiledGuid
+ );
+ const initialmenuCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.menuGuid
+ );
+
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.BOOKMARKS,
+ PROFILE
+ );
+ const postToolbarCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.toolbarGuid
+ );
+ const postUnfiledCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.unfiledGuid
+ );
+ const postmenuCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.menuGuid
+ );
+ Assert.equal(
+ postUnfiledCount - initialUnfiledCount,
+ 105,
+ "Should have seen 105 items in unsorted bookmarks"
+ );
+ Assert.equal(
+ postToolbarCount - initialToolbarCount,
+ 105,
+ "Should have seen 105 items in toolbar"
+ );
+ Assert.equal(
+ postmenuCount - initialmenuCount,
+ 0,
+ "Should have seen 0 items in menu toolbar"
+ );
+
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+
+ Assert.equal(itemsSeen.bookmarks, 200, "Should have seen 200 bookmarks.");
+ Assert.equal(itemsSeen.folders, 10, "Should have seen 10 folders.");
+ Assert.equal(
+ MigrationUtils._importQuantities.bookmarks,
+ itemsSeen.bookmarks + itemsSeen.folders,
+ "Telemetry reporting correct."
+ );
+ Assert.ok(observerNotified, "The observer should be notified upon migration");
+}
+
+add_task(async function test_Chrome() {
+ let subDirs =
+ AppConstants.platform == "linux" ? ["google-chrome"] : ["Google", "Chrome"];
+ await testBookmarks("chrome", subDirs);
+});
+
+add_task(async function test_ChromiumEdge() {
+ if (AppConstants.platform == "linux") {
+ // Edge isn't available on Linux.
+ return;
+ }
+ let subDirs =
+ AppConstants.platform == "macosx"
+ ? ["Microsoft Edge"]
+ : ["Microsoft", "Edge"];
+ await testBookmarks("chromium-edge", subDirs);
+});
+
+async function getFolderItemCount(guid) {
+ let results = await PlacesUtils.promiseBookmarksTree(guid);
+
+ return results.itemsCount;
+}
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 0000000000..b738c1d1bc
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_cookies.js
@@ -0,0 +1,72 @@
+"use strict";
+
+const { ForgetAboutSite } = ChromeUtils.import(
+ "resource://gre/modules/ForgetAboutSite.jsm"
+);
+
+add_task(async function() {
+ registerFakePath("ULibDir", do_get_file("Library/"));
+ let migrator = await MigrationUtils.getMigrator("chrome");
+
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "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.
+ await 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 cookies = Services.cookies.getCookiesFromHost(COOKIE.host, {});
+ Assert.ok(cookies.length, "Cookies available");
+ let foundCookie = cookies[0];
+
+ for (let prop of Object.keys(COOKIE)) {
+ Assert.equal(foundCookie[prop], COOKIE[prop], "Check cookie " + prop);
+ }
+
+ // Cleanup.
+ await 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_history.js b/browser/components/migration/tests/unit/test_Chrome_history.js
new file mode 100644
index 0000000000..c88a6380c2
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_history.js
@@ -0,0 +1,206 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ChromeMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMigrationUtils.sys.mjs"
+);
+
+const SOURCE_PROFILE_DIR = "Library/Application Support/Google/Chrome/Default/";
+
+const PROFILE = {
+ id: "Default",
+ name: "Person 1",
+};
+
+/**
+ * TEST_URLS reflects the data stored in '${SOURCE_PROFILE_DIR}HistoryMaster'.
+ * The main object reflects the data in the 'urls' table. The visits property
+ * reflects the associated data in the 'visits' table.
+ */
+const TEST_URLS = [
+ {
+ id: 1,
+ url: "http://example.com/",
+ title: "test",
+ visit_count: 1,
+ typed_count: 0,
+ last_visit_time: 13193151310368000,
+ hidden: 0,
+ visits: [
+ {
+ id: 1,
+ url: 1,
+ visit_time: 13193151310368000,
+ from_visit: 0,
+ transition: 805306370,
+ segment_id: 0,
+ visit_duration: 10745006,
+ incremented_omnibox_typed_score: 0,
+ },
+ ],
+ },
+ {
+ id: 2,
+ url: "http://invalid.com/",
+ title: "test2",
+ visit_count: 1,
+ typed_count: 0,
+ last_visit_time: 13193154948901000,
+ hidden: 0,
+ visits: [
+ {
+ id: 2,
+ url: 2,
+ visit_time: 13193154948901000,
+ from_visit: 0,
+ transition: 805306376,
+ segment_id: 0,
+ visit_duration: 6568270,
+ incremented_omnibox_typed_score: 0,
+ },
+ ],
+ },
+];
+
+async function setVisitTimes(time) {
+ let loginDataFile = do_get_file(`${SOURCE_PROFILE_DIR}History`);
+ let dbConn = await Sqlite.openConnection({ path: loginDataFile.path });
+
+ await dbConn.execute(`UPDATE urls SET last_visit_time = :last_visit_time`, {
+ last_visit_time: time,
+ });
+ await dbConn.execute(`UPDATE visits SET visit_time = :visit_time`, {
+ visit_time: time,
+ });
+
+ await dbConn.close();
+}
+
+function setExpectedVisitTimes(time) {
+ for (let urlInfo of TEST_URLS) {
+ urlInfo.last_visit_time = time;
+ urlInfo.visits[0].visit_time = time;
+ }
+}
+
+function assertEntryMatches(entry, urlInfo, dateWasInFuture = false) {
+ info(`Checking url: ${urlInfo.url}`);
+ Assert.ok(entry, `Should have stored an entry`);
+
+ Assert.equal(entry.url, urlInfo.url, "Should have the correct URL");
+ Assert.equal(entry.title, urlInfo.title, "Should have the correct title");
+ Assert.equal(
+ entry.visits.length,
+ urlInfo.visits.length,
+ "Should have the correct number of visits"
+ );
+
+ for (let index in urlInfo.visits) {
+ Assert.equal(
+ entry.visits[index].transition,
+ PlacesUtils.history.TRANSITIONS.LINK,
+ "Should have Link type transition"
+ );
+
+ if (dateWasInFuture) {
+ Assert.lessOrEqual(
+ entry.visits[index].date.getTime(),
+ new Date().getTime(),
+ "Should have moved the date to no later than the current date."
+ );
+ } else {
+ Assert.equal(
+ entry.visits[index].date.getTime(),
+ ChromeMigrationUtils.chromeTimeToDate(
+ urlInfo.visits[index].visit_time,
+ new Date()
+ ).getTime(),
+ "Should have the correct date"
+ );
+ }
+ }
+}
+
+function setupHistoryFile() {
+ removeHistoryFile();
+ let file = do_get_file(`${SOURCE_PROFILE_DIR}HistoryMaster`);
+ file.copyTo(file.parent, "History");
+}
+
+function removeHistoryFile() {
+ let file = do_get_file(`${SOURCE_PROFILE_DIR}History`, true);
+ try {
+ file.remove(false);
+ } catch (ex) {
+ // It is ok if this doesn't exist.
+ if (ex.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
+ throw ex;
+ }
+ }
+}
+
+add_task(async function setup() {
+ registerFakePath("ULibDir", do_get_file("Library/"));
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ removeHistoryFile();
+ });
+});
+
+add_task(async function test_import() {
+ setupHistoryFile();
+ await PlacesUtils.history.clear();
+ // Update to ~10 days ago since the date can't be too old or Places may expire it.
+ const pastDate = new Date(new Date().getTime() - 1000 * 60 * 60 * 24 * 10);
+ const pastChromeTime = ChromeMigrationUtils.dateToChromeTime(pastDate);
+ await setVisitTimes(pastChromeTime);
+ setExpectedVisitTimes(pastChromeTime);
+
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.HISTORY,
+ PROFILE
+ );
+
+ for (let urlInfo of TEST_URLS) {
+ let entry = await PlacesUtils.history.fetch(urlInfo.url, {
+ includeVisits: true,
+ });
+ assertEntryMatches(entry, urlInfo);
+ }
+});
+
+add_task(async function test_import_future_date() {
+ setupHistoryFile();
+ await PlacesUtils.history.clear();
+ const futureDate = new Date().getTime() + 6000 * 60 * 24;
+ await setVisitTimes(ChromeMigrationUtils.dateToChromeTime(futureDate));
+
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.HISTORY,
+ PROFILE
+ );
+
+ for (let urlInfo of TEST_URLS) {
+ let entry = await PlacesUtils.history.fetch(urlInfo.url, {
+ includeVisits: true,
+ });
+ assertEntryMatches(entry, urlInfo, true);
+ }
+});
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 0000000000..d98f4cd039
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_passwords.js
@@ -0,0 +1,379 @@
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+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",
+ origin: "https://c9.io",
+ formActionOrigin: "https://c9.io",
+ httpRealm: null,
+ usernameField: "inputEmail",
+ passwordField: "inputPassword",
+ timeCreated: 1437418416037,
+ timePasswordChanged: 1437418416037,
+ timesUsed: 1,
+ },
+ {
+ id: 2,
+ username: "username@gmail.com",
+ password: "password2",
+ origin: "https://accounts.google.com",
+ formActionOrigin: "https://accounts.google.com",
+ httpRealm: null,
+ usernameField: "Email",
+ passwordField: "Passwd",
+ timeCreated: 1437418446598,
+ timePasswordChanged: 1437418446598,
+ timesUsed: 6,
+ },
+ {
+ id: 3,
+ username: "username",
+ password: "password3",
+ origin: "https://www.facebook.com",
+ formActionOrigin: "https://www.facebook.com",
+ httpRealm: null,
+ usernameField: "email",
+ passwordField: "pass",
+ timeCreated: 1437418478851,
+ timePasswordChanged: 1437418478851,
+ timesUsed: 1,
+ },
+ {
+ id: 4,
+ username: "user",
+ password: "اقرأPÀßwörd",
+ origin: "http://httpbin.org",
+ formActionOrigin: null,
+ httpRealm: "me@kennethreitz.com", // Digest auth.
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1437787462368,
+ timePasswordChanged: 1437787462368,
+ timesUsed: 1,
+ },
+ {
+ id: 5,
+ username: "buser",
+ password: "bpassword",
+ origin: "http://httpbin.org",
+ formActionOrigin: null,
+ httpRealm: "Fake Realm", // Basic auth.
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1437787539233,
+ timePasswordChanged: 1437787539233,
+ timesUsed: 1,
+ },
+ {
+ id: 6,
+ username: "username",
+ password: "password6",
+ origin: "https://www.example.com",
+ formActionOrigin: "", // NULL `action_url`
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "pass",
+ timeCreated: 1557291348878,
+ timePasswordChanged: 1557291348878,
+ timesUsed: 1,
+ },
+ {
+ id: 7,
+ version: "v10",
+ username: "username",
+ password: "password",
+ origin: "https://v10.io",
+ formActionOrigin: "https://v10.io",
+ httpRealm: null,
+ usernameField: "inputEmail",
+ passwordField: "inputPassword",
+ timeCreated: 1437418416037,
+ timePasswordChanged: 1437418416037,
+ timesUsed: 1,
+ },
+];
+
+var loginCrypto;
+var dbConn;
+
+async function promiseSetPassword(login) {
+ let encryptedString = await loginCrypto.encryptData(
+ login.password,
+ login.version
+ );
+ info(`promiseSetPassword: ${encryptedString}`);
+ let passwordValue = new Uint8Array(
+ loginCrypto.stringToArray(encryptedString)
+ );
+ return dbConn.execute(
+ `UPDATE logins
+ SET password_value = :password_value
+ WHERE rowid = :rowid
+ `,
+ { password_value: passwordValue, rowid: login.id }
+ );
+}
+
+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.origin,
+ chromeLogin.origin,
+ "The two logins ID " + id + " have the same origin"
+ );
+ Assert.equal(
+ passwordManagerLogin.formActionOrigin,
+ chromeLogin.formActionOrigin,
+ "The two logins ID " + id + " have the same formActionOrigin"
+ );
+ 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.origin,
+ login.formActionOrigin,
+ 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(async function setup() {
+ let dirSvcPath;
+ let pathId;
+ let profilePathSegments;
+
+ // Use a mock service and account name to avoid a Keychain auth. prompt that
+ // would block the test from finishing if Chrome has already created a matching
+ // Keychain entry. This allows us to still exercise the keychain lookup code.
+ // The mock encryption passphrase is used when the Keychain item isn't found.
+ let mockMacOSKeychain = {
+ passphrase: "bW96aWxsYWZpcmVmb3g=",
+ serviceName: "TESTING Chrome Safe Storage",
+ accountName: "TESTING Chrome",
+ };
+ if (AppConstants.platform == "macosx") {
+ let { ChromeMacOSLoginCrypto } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMacOSLoginCrypto.sys.mjs"
+ );
+ loginCrypto = new ChromeMacOSLoginCrypto(
+ mockMacOSKeychain.serviceName,
+ mockMacOSKeychain.accountName,
+ mockMacOSKeychain.passphrase
+ );
+ dirSvcPath = "Library/";
+ pathId = "ULibDir";
+ profilePathSegments = [
+ "Application Support",
+ "Google",
+ "Chrome",
+ "Default",
+ "Login Data",
+ ];
+ } else if (AppConstants.platform == "win") {
+ let { ChromeWindowsLoginCrypto } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeWindowsLoginCrypto.sys.mjs"
+ );
+ loginCrypto = new ChromeWindowsLoginCrypto("Chrome");
+ dirSvcPath = "AppData/Local/";
+ pathId = "LocalAppData";
+ profilePathSegments = [
+ "Google",
+ "Chrome",
+ "User Data",
+ "Default",
+ "Login Data",
+ ];
+ } else {
+ throw new Error("Not implemented");
+ }
+ let dirSvcFile = do_get_file(dirSvcPath);
+ registerFakePath(pathId, dirSvcFile);
+
+ // We don't import osfile.jsm until after registering the fake path, because
+ // importing osfile will sometimes greedily fetch certain path identifiers
+ // from the dir service, which means they get cached, which means we can't
+ // register a fake path for them anymore.
+ const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+ info(OS.Path.join(dirSvcFile.path, ...profilePathSegments));
+ let loginDataFilePath = OS.Path.join(dirSvcFile.path, ...profilePathSegments);
+ dbConn = await Sqlite.openConnection({ path: loginDataFilePath });
+
+ if (AppConstants.platform == "macosx") {
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ Object.assign(migrator, {
+ _keychainServiceName: mockMacOSKeychain.serviceName,
+ _keychainAccountName: mockMacOSKeychain.accountName,
+ _keychainMockPassphrase: mockMacOSKeychain.passphrase,
+ });
+ }
+
+ registerCleanupFunction(() => {
+ Services.logins.removeAllUserFacingLogins();
+ if (loginCrypto.finalize) {
+ loginCrypto.finalize();
+ }
+ return dbConn.close();
+ });
+});
+
+add_task(async function test_importIntoEmptyDB() {
+ for (let login of TEST_LOGINS) {
+ await promiseSetPassword(login);
+ }
+
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 0, "There are no logins initially");
+
+ // Migrate the logins.
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.PASSWORDS,
+ PROFILE,
+ true
+ );
+
+ 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(async function test_importExistingLogins() {
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ Services.logins.removeAllUserFacingLogins();
+ 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.
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.PASSWORDS,
+ PROFILE,
+ true
+ );
+
+ 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_Chrome_passwords_emptySource.js b/browser/components/migration/tests/unit/test_Chrome_passwords_emptySource.js
new file mode 100644
index 0000000000..777a348116
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_passwords_emptySource.js
@@ -0,0 +1,83 @@
+"use strict";
+
+/**
+ * Ensure that there is no dialog for OS crypto that blocks a migration when
+ * importing from an empty Login Data DB.
+ */
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const PROFILE = {
+ id: "Default",
+ name: "Person With No Data",
+};
+
+add_task(async function setup() {
+ let dirSvcPath;
+ let pathId;
+
+ // Use a mock service and account name to avoid a Keychain auth. prompt that
+ // would block the test from finishing if Chrome has already created a matching
+ // Keychain entry. This allows us to still exercise the keychain lookup code.
+ // The mock encryption passphrase is used when the Keychain item isn't found.
+ let mockMacOSKeychain = {
+ passphrase: "bW96aWxsYWZpcmVmb3g=",
+ serviceName: "TESTING Chrome Safe Storage",
+ accountName: "TESTING Chrome",
+ };
+ if (AppConstants.platform == "macosx") {
+ dirSvcPath = "LibraryWithNoData/";
+ pathId = "ULibDir";
+ } else if (AppConstants.platform == "win") {
+ dirSvcPath = "AppData/LocalWithNoData/";
+ pathId = "LocalAppData";
+ } else {
+ throw new Error("Not implemented");
+ }
+ let dirSvcFile = do_get_file(dirSvcPath);
+ registerFakePath(pathId, dirSvcFile);
+
+ if (AppConstants.platform == "macosx") {
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ Object.assign(migrator, {
+ _keychainServiceName: mockMacOSKeychain.serviceName,
+ _keychainAccountName: mockMacOSKeychain.accountName,
+ // No `_keychainMockPassphrase` as we don't want to mock the OS dialog as
+ // it shouldn't appear.
+ });
+ }
+
+ registerCleanupFunction(() => {
+ Services.logins.removeAllUserFacingLogins();
+ });
+});
+
+add_task(async function test_importEmptyDBWithoutAuthPrompts() {
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 0, "There are no logins initially");
+
+ // Migrate the logins. If an OS dialog (e.g. Keychain) blocks this the test
+ // would timeout.
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.PASSWORDS,
+ PROFILE,
+ true
+ );
+
+ logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 0, "Check login count after importing the data");
+ Assert.equal(
+ logins.length,
+ MigrationUtils._importQuantities.logins,
+ "Check telemetry matches the actual import."
+ );
+});
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 0000000000..0bb81237cc
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Edge_db_migration.js
@@ -0,0 +1,773 @@
+"use strict";
+
+const { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
+const {
+ ESE,
+ KERNEL,
+ gLibs,
+ COLUMN_TYPES,
+ declareESEFunction,
+ loadLibraries,
+} = ChromeUtils.importESModule("resource:///modules/ESEDBReader.sys.mjs");
+const { EdgeProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/EdgeProfileMigrator.sys.mjs"
+);
+
+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, tables) {
+ if (!initializedESE) {
+ initializedESE = true;
+ loadLibraries();
+
+ KERNEL.SystemTimeToFileTime = gLibs.kernel.declare(
+ "SystemTimeToFileTime",
+ ctypes.winapi_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;
+
+ for (let [tableName, data] of tables) {
+ let { rows, columns } = data;
+ 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(async 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 bookmarkReferenceItems = [
+ {
+ 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,
+ },
+ ];
+
+ let readingListReferenceItems = [
+ {
+ Title: "Some mozilla page",
+ URL: "http://www.mozilla.org/somepage/",
+ AddedDate: new Date(creationDate.valueOf() + 900),
+ ItemId: "c88426fd-52a7-419d-acbc-d2310e8afebe",
+ IsDeleted: false,
+ },
+ {
+ Title: "Some other page",
+ URL: "https://www.example.org/somepage/",
+ AddedDate: new Date(creationDate.valueOf() + 1000),
+ ItemId: "a35fc843-5d5a-4d1e-9be8-45214be24b5c",
+ IsDeleted: false,
+ },
+ ];
+ eseDBWritingHelpers.setupDB(
+ db,
+ new Map([
+ [
+ "Favorites",
+ {
+ columns: [
+ { 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" },
+ ],
+ rows: bookmarkReferenceItems,
+ },
+ ],
+ [
+ "ReadingList",
+ {
+ columns: [
+ { type: COLUMN_TYPES.JET_coltypLongText, name: "URL", cbMax: 4096 },
+ {
+ type: COLUMN_TYPES.JET_coltypLongText,
+ name: "Title",
+ cbMax: 4096,
+ },
+ { type: COLUMN_TYPES.JET_coltypLongLong, name: "AddedDate" },
+ { type: COLUMN_TYPES.JET_coltypGUID, name: "ItemId" },
+ { type: COLUMN_TYPES.JET_coltypBit, name: "IsDeleted" },
+ ],
+ rows: readingListReferenceItems,
+ },
+ ],
+ ])
+ );
+
+ // Manually create an EdgeProfileMigrator rather than going through
+ // MigrationUtils.getMigrator to avoid the user data availability check, since
+ // we're mocking out that stuff.
+ let migrator = new EdgeProfileMigrator();
+ let bookmarksMigrator = migrator.getBookmarksMigratorForTesting(db);
+ Assert.ok(bookmarksMigrator.exists, "Should recognize db we just created");
+
+ let seenBookmarks = [];
+ let listener = events => {
+ for (let event of events) {
+ let {
+ id,
+ itemType,
+ url,
+ title,
+ dateAdded,
+ guid,
+ index,
+ parentGuid,
+ parentId,
+ } = event;
+ if (title.startsWith("Deleted")) {
+ ok(false, "Should not see deleted items being bookmarked!");
+ }
+ seenBookmarks.push({
+ id,
+ parentId,
+ index,
+ itemType,
+ url,
+ title,
+ dateAdded,
+ guid,
+ parentGuid,
+ });
+ }
+ };
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+
+ let migrateResult = await 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.observers.removeListener(["bookmark-added"], listener);
+ Assert.ok(migrateResult, "Migration should succeed");
+ Assert.equal(
+ seenBookmarks.length,
+ 5,
+ "Should have seen 5 items being bookmarked."
+ );
+ Assert.equal(
+ seenBookmarks.length,
+ MigrationUtils._importQuantities.bookmarks,
+ "Telemetry should have items"
+ );
+
+ let menuParents = seenBookmarks.filter(
+ item => item.parentGuid == PlacesUtils.bookmarks.menuGuid
+ );
+ Assert.equal(
+ menuParents.length,
+ 3,
+ "Bookmarks are added to the menu without a folder"
+ );
+ 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 = PlacesUtils.bookmarks.menuGuid;
+ let toolbarParentGuid = PlacesUtils.bookmarks.toolbarGuid;
+
+ let expectedTitlesInMenu = bookmarkReferenceItems
+ .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 = bookmarkReferenceItems
+ .filter(item => item.ParentId == "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf")
+ .map(item => item.Title);
+
+ for (let bookmark of seenBookmarks) {
+ let shouldBeInMenu = expectedTitlesInMenu.includes(bookmark.title);
+ let shouldBeInToolbar = expectedTitlesInToolbar.includes(bookmark.title);
+ if (bookmark.title == "Folder") {
+ 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.guid == menuParentGuid ||
+ bookmark.guid == 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.guid == bookmark.parentGuid
+ );
+ Assert.equal(
+ parent && parent.title,
+ "Folder",
+ "Subfoldered item should be in subfolder labeled 'Folder'"
+ );
+ }
+
+ let dbItem = bookmarkReferenceItems.find(
+ someItem => bookmark.title == someItem.Title
+ );
+ if (!dbItem) {
+ Assert.ok(
+ [menuParentGuid, toolbarParentGuid].includes(bookmark.guid),
+ "This item should be one of the containers"
+ );
+ } else {
+ Assert.equal(dbItem.URL || "", bookmark.url, "URL is correct");
+ Assert.equal(
+ dbItem.DateUpdated.valueOf(),
+ new Date(bookmark.dateAdded).valueOf(),
+ "Date added is correct"
+ );
+ }
+ }
+
+ MigrationUtils._importQuantities.bookmarks = 0;
+ seenBookmarks = [];
+ listener = events => {
+ for (let event of events) {
+ let {
+ id,
+ itemType,
+ url,
+ title,
+ dateAdded,
+ guid,
+ index,
+ parentGuid,
+ parentId,
+ } = event;
+ seenBookmarks.push({
+ id,
+ parentId,
+ index,
+ itemType,
+ url,
+ title,
+ dateAdded,
+ guid,
+ parentGuid,
+ });
+ }
+ };
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+
+ let readingListMigrator = migrator.getReadingListMigratorForTesting(db);
+ Assert.ok(readingListMigrator.exists, "Should recognize db we just created");
+ migrateResult = await new Promise(resolve =>
+ readingListMigrator.migrate(resolve)
+ ).catch(ex => {
+ Cu.reportError(ex);
+ Assert.ok(false, "Got an exception trying to migrate data! " + ex);
+ return false;
+ });
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+ Assert.ok(migrateResult, "Migration should succeed");
+ Assert.equal(
+ seenBookmarks.length,
+ 3,
+ "Should have seen 3 items being bookmarked (2 items + 1 folder)."
+ );
+ Assert.equal(
+ seenBookmarks.length,
+ MigrationUtils._importQuantities.bookmarks,
+ "Telemetry should have items"
+ );
+ let readingListContainerLabel = await MigrationUtils.getLocalizedString(
+ "imported-edge-reading-list"
+ );
+
+ for (let bookmark of seenBookmarks) {
+ if (readingListContainerLabel == bookmark.title) {
+ continue;
+ }
+ let referenceItem = readingListReferenceItems.find(
+ item => item.Title == bookmark.title
+ );
+ Assert.ok(referenceItem, "Should have imported what we expected");
+ Assert.equal(referenceItem.URL, bookmark.url, "Should have the right URL");
+ readingListReferenceItems.splice(
+ readingListReferenceItems.findIndex(item => item.Title == bookmark.title),
+ 1
+ );
+ }
+ Assert.ok(
+ !readingListReferenceItems.length,
+ "Should have seen all expected items."
+ );
+});
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 0000000000..ed5f27e14f
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_IE7_passwords.js
@@ -0,0 +1,1371 @@
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "OSCrypto",
+ "resource://gre/modules/OSCrypto_win.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",
+ origin: "https://twitter.com",
+ formActionOrigin: "",
+ 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",
+ origin: "https://www.facebook.com",
+ formActionOrigin: "",
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1439326966000,
+ timeLastUsed: 1439326966000,
+ timePasswordChanged: 1439326966000,
+ timesUsed: 1,
+ },
+ {
+ username: "username1",
+ password: "password1",
+ origin: "https://www.facebook.com",
+ formActionOrigin: "",
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1439326997000,
+ timeLastUsed: 1439326997000,
+ timePasswordChanged: 1439326997000,
+ timesUsed: 1,
+ },
+ {
+ username: "username2",
+ password: "password2",
+ origin: "https://www.facebook.com",
+ formActionOrigin: "",
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1439327040000,
+ timeLastUsed: 1439327040000,
+ timePasswordChanged: 1439327040000,
+ timesUsed: 1,
+ },
+ {
+ username: "username3",
+ password: "password3",
+ origin: "https://www.facebook.com",
+ formActionOrigin: "",
+ 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",
+ origin: "https://login.live.com",
+ formActionOrigin: "",
+ 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: [
+ // This login is present in the data, but should be stripped out
+ // by the validation rules of the importer:
+ // {
+ // "username": "a",
+ // "password": "",
+ // "origin": "http://www.reddit.com",
+ // "formActionOrigin": "",
+ // "httpRealm": null,
+ // "usernameField": "",
+ // "passwordField": ""
+ // },
+ {
+ username: "購読を",
+ password: "Bitte bestätigen",
+ origin: "http://www.reddit.com",
+ formActionOrigin: "",
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1439340874000,
+ timeLastUsed: 1439340874000,
+ timePasswordChanged: 1439340874000,
+ timesUsed: 1,
+ },
+ {
+ username: "重置密码",
+ password: "Пароль успешно сброшен. Войти",
+ origin: "http://www.reddit.com",
+ formActionOrigin: "",
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1439340971000,
+ timeLastUsed: 1439340971000,
+ timePasswordChanged: 1439340971000,
+ timesUsed: 1,
+ },
+ {
+ username: "بسم الله الرحمن",
+ password: "veuillez le vérifier à nouveau",
+ origin: "http://www.reddit.com",
+ formActionOrigin: "",
+ 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();
+ }
+}
+
+async function getFirstResourceOfType(type) {
+ let migrator = await MigrationUtils.getMigrator("ie");
+ 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);
+}
+
+add_task(async function setup() {
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
+ await Assert.rejects(
+ getFirstResourceOfType(MigrationUtils.resourceTypes.PASSWORDS),
+ /failed to find/,
+ "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(async function test_passwordsNotAvailable() {
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
+ return;
+ }
+
+ let migrator = await 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
+ await 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(async 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
+
+ registerCleanupFunction(() => {
+ Services.logins.removeAllUserFacingLogins();
+ 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 = await 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)
+ );
+ Assert.ok(migrator.exists, "The migrator has to exist");
+ uris.push(website.uri);
+ hashes.push(website.hash);
+
+ await 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 0000000000..4df0c7737c
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_IE_bookmarks.js
@@ -0,0 +1,30 @@
+"use strict";
+
+add_task(async function() {
+ let migrator = await MigrationUtils.getMigrator("ie");
+ // Sanity check for the source.
+ Assert.ok(await migrator.isSourceAvailable(), "Check migrator source");
+
+ // Since this test doesn't mock out the favorites, execution is dependent
+ // on the actual favorites stored on the local machine's IE favorites database.
+ // As such, we can't assert that bookmarks were migrated to both the bookmarks
+ // menu and the bookmarks toolbar.
+ let itemCount = 0;
+ let listener = events => {
+ for (let event of events) {
+ if (event.itemType == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
+ info("bookmark added: " + event.parentGuid);
+ itemCount++;
+ }
+ }
+ };
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+ Assert.equal(
+ MigrationUtils._importQuantities.bookmarks,
+ itemCount,
+ "Ensure telemetry matches actual number of imported items."
+ );
+});
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 0000000000..bb003ad5a5
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_IE_cookies.js
@@ -0,0 +1,149 @@
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "ctypes",
+ "resource://gre/modules/ctypes.jsm"
+);
+
+add_task(async function() {
+ let migrator = await MigrationUtils.getMigrator("ie");
+ // Sanity check for the source.
+ Assert.ok(await migrator.isSourceAvailable());
+
+ // Windows versions newer than 1709 don't store cookies as files anymore,
+ // thus our migrators don't import anything and this test is pointless.
+ // In these versions the CookD folder contains a deprecated.cookie file.
+ let deprecatedCookie = Services.dirsvc.get("CookD", Ci.nsIFile);
+ deprecatedCookie.append("deprecated.cookie");
+ if (deprecatedCookie.exists()) {
+ return;
+ }
+
+ 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
+ );
+ */
+ // NOTE: Even though MSDN documentation does not indicate a calling convention,
+ // InternetSetCookieW is declared in SDK headers as __stdcall but is exported
+ // from wininet.dll without name mangling, so it is effectively winapi_abi
+ let setIECookie = wininet.declare(
+ "InternetSetCookieW",
+ ctypes.winapi_abi,
+ BOOL,
+ LPCTSTR,
+ LPCTSTR,
+ LPCTSTR
+ );
+
+ /*
+ BOOL InternetGetCookieW(
+ _In_ LPCTSTR lpszUrl,
+ _In_ LPCTSTR lpszCookieName,
+ _Out_ LPCTSTR lpszCookieData,
+ _Inout_ LPDWORD lpdwSize
+ );
+ */
+ // NOTE: Even though MSDN documentation does not indicate a calling convention,
+ // InternetGetCookieW is declared in SDK headers as __stdcall but is exported
+ // from wininet.dll without name mangling, so it is effectively winapi_abi
+ let getIECookie = wininet.declare(
+ "InternetGetCookieW",
+ ctypes.winapi_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();
+
+ registerCleanupFunction(() => {
+ // 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"
+ );
+ info("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.
+ await 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 cookies = Services.cookies.getCookiesFromHost(COOKIE.host, {});
+ Assert.ok(cookies.length);
+ let foundCookie = cookies[0];
+
+ 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_IE_history.js b/browser/components/migration/tests/unit/test_IE_history.js
new file mode 100644
index 0000000000..fd803f4607
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_IE_history.js
@@ -0,0 +1,50 @@
+"use strict";
+
+// These match what we add to IE via InsertIEHistory.exe.
+const TEST_ENTRIES = [
+ {
+ url: "http://www.mozilla.org/1",
+ title: "Mozilla HTTP Test",
+ },
+ {
+ url: "https://www.mozilla.org/2",
+ // Test character encoding with a fox emoji:
+ title: "Mozilla HTTPS Test 🦊",
+ },
+];
+
+function insertIEHistory() {
+ let file = do_get_file("InsertIEHistory.exe", false);
+ let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(file);
+
+ let args = [];
+ process.run(true, args, args.length);
+
+ Assert.ok(!process.isRunning, "Should be done running");
+ Assert.equal(process.exitValue, 0, "Check exit code");
+}
+
+add_task(async function setup() {
+ await PlacesUtils.history.clear();
+
+ insertIEHistory();
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function test_IE_history() {
+ let migrator = await MigrationUtils.getMigrator("ie");
+ Assert.ok(await migrator.isSourceAvailable(), "Source is available");
+
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY);
+
+ for (let { url, title } of TEST_ENTRIES) {
+ let entry = await PlacesUtils.history.fetch(url, { includeVisits: true });
+ Assert.equal(entry.url, url, "Should have the correct URL");
+ Assert.equal(entry.title, title, "Should have the correct title");
+ Assert.ok(!!entry.visits.length, "Should have some visits");
+ }
+});
diff --git a/browser/components/migration/tests/unit/test_MigrationUtils_timedRetry.js b/browser/components/migration/tests/unit/test_MigrationUtils_timedRetry.js
new file mode 100644
index 0000000000..546b7bae88
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_MigrationUtils_timedRetry.js
@@ -0,0 +1,25 @@
+"use strict";
+
+let tmpFile = FileUtils.getDir("TmpD", [], true);
+let dbConn;
+
+add_task(async function setup() {
+ tmpFile.append("TestDB");
+ dbConn = await Sqlite.openConnection({ path: tmpFile.path });
+
+ registerCleanupFunction(async () => {
+ await dbConn.close();
+ IOUtils.remove(tmpFile.path);
+ });
+});
+
+add_task(async function testgetRowsFromDBWithoutLocksRetries() {
+ let promise = MigrationUtils.getRowsFromDBWithoutLocks(
+ tmpFile.path,
+ "Temp DB",
+ "SELECT * FROM moz_temp_table"
+ );
+ await new Promise(resolve => do_timeout(50, resolve));
+ dbConn.execute("CREATE TABLE moz_temp_table (id INTEGER PRIMARY KEY)");
+ await promise;
+});
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 0000000000..b316165871
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Safari_bookmarks.js
@@ -0,0 +1,69 @@
+"use strict";
+
+const { CustomizableUI } = ChromeUtils.import(
+ "resource:///modules/CustomizableUI.jsm"
+);
+
+add_task(async function() {
+ registerFakePath("ULibDir", do_get_file("Library/"));
+
+ let migrator = await MigrationUtils.getMigrator("safari");
+ // Sanity check for the source.
+ Assert.ok(await migrator.isSourceAvailable());
+
+ // Wait for the imported bookmarks. We don't check that "From Safari"
+ // folders are created on the toolbar since the profile
+ // we're importing to has less than 3 bookmarks in the destination
+ // so a "From Safari" folder isn't created.
+ let expectedParentGuids = [PlacesUtils.bookmarks.toolbarGuid];
+ let itemCount = 0;
+
+ let gotFolder = false;
+ let listener = events => {
+ for (let event of events) {
+ itemCount++;
+ if (
+ event.itemType == PlacesUtils.bookmarks.TYPE_FOLDER &&
+ event.title == "Stuff"
+ ) {
+ gotFolder = true;
+ }
+ if (expectedParentGuids.length) {
+ let index = expectedParentGuids.indexOf(event.parentGuid);
+ Assert.ok(index != -1, "Found expected parent");
+ expectedParentGuids.splice(index, 1);
+ }
+ }
+ };
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+ let observerNotified = false;
+ Services.obs.addObserver((aSubject, aTopic, aData) => {
+ let [toolbar, visibility] = JSON.parse(aData);
+ Assert.equal(
+ toolbar,
+ CustomizableUI.AREA_BOOKMARKS,
+ "Notification should be received for bookmarks toolbar"
+ );
+ Assert.equal(
+ visibility,
+ "true",
+ "Notification should say to reveal the bookmarks toolbar"
+ );
+ observerNotified = true;
+ }, "browser-set-toolbar-visibility");
+
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+
+ // Check the bookmarks have been imported to all the expected parents.
+ Assert.ok(!expectedParentGuids.length, "No more expected parents");
+ Assert.ok(gotFolder, "Should have seen the folder get imported");
+ Assert.equal(itemCount, 13, "Should import all 13 items.");
+ // Check that the telemetry matches:
+ Assert.equal(
+ MigrationUtils._importQuantities.bookmarks,
+ itemCount,
+ "Telemetry reporting correct."
+ );
+ Assert.ok(observerNotified, "The observer should be notified upon migration");
+});
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 0000000000..5be6b740fc
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_fx_telemetry.js
@@ -0,0 +1,269 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+const { FirefoxProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/FirefoxProfileMigrator.sys.mjs"
+);
+
+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();
+ for (let file of dir.directoryEntries) {
+ 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;
+}
+
+async function promiseMigrator(name, srcDir, targetDir) {
+ // As the FirefoxProfileMigrator is a startup-only migrator, we import its
+ // module and instantiate it directly rather than going through MigrationUtils,
+ // to bypass that availability check.
+ let migrator = new FirefoxProfileMigrator();
+ 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(async function test_empty() {
+ let [srcDir, targetDir] = getTestDirs();
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true with empty directories");
+ // check both are empty
+ checkDirectoryContains(srcDir, {});
+ checkDirectoryContains(targetDir, {});
+});
+
+add_task(async 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 = await 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(async function test_datareporting_not_dir() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ writeToFile(srcDir, "datareporting", "I'm a file but should be a directory");
+
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(
+ ok,
+ "callback should have been true even though the directory was a file"
+ );
+
+ checkDirectoryContains(targetDir, {});
+});
+
+add_task(async function test_datareporting_empty() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Migrate with an empty 'datareporting' subdir.
+ createSubDir(srcDir, "datareporting");
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ // We should end up with no migrated files.
+ checkDirectoryContains(targetDir, {
+ datareporting: {},
+ });
+});
+
+add_task(async function test_healthreport_empty() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Migrate with no 'datareporting' and an empty 'healthreport' subdir.
+ createSubDir(srcDir, "healthreport");
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ // We should end up with no migrated files.
+ checkDirectoryContains(targetDir, {});
+});
+
+add_task(async 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 = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ checkDirectoryContains(targetDir, {
+ datareporting: {
+ "state.json": shouldBeCopied,
+ "session-state.json": shouldBeCopied,
+ },
+ });
+});
+
+add_task(async 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 = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ checkDirectoryContains(targetDir, {
+ datareporting: {
+ "state.json": stateContent,
+ },
+ });
+});
+
+add_task(async 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 = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ checkDirectoryContains(targetDir, {
+ datareporting: {
+ "session-state.json": sessionStateContent,
+ },
+ });
+});
+
+add_task(async 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 = await 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 0000000000..7f5779ef71
--- /dev/null
+++ b/browser/components/migration/tests/unit/xpcshell.ini
@@ -0,0 +1,49 @@
+[DEFAULT]
+head = head_migration.js
+tags = condprof
+firefox-appdir = browser
+skip-if = toolkit == 'android' # bug 1730213
+prefs =
+ browser.migrate.showBookmarksToolbarAfterMigration=true
+support-files =
+ Library/**
+ AppData/**
+
+[test_360se_bookmarks.js]
+skip-if = os != "win"
+[test_360seMigrationUtils.js]
+run-if = os == "win"
+[test_Chrome_bookmarks.js]
+[test_Chrome_cookies.js]
+skip-if = os != "mac" # Relies on ULibDir
+[test_Chrome_history.js]
+skip-if = os != "mac" # Relies on ULibDir
+[test_Chrome_passwords.js]
+skip-if = os != "win" && os != "mac"
+ condprof # bug 1769154 - not realistic for condprof
+[test_Chrome_passwords_emptySource.js]
+skip-if = os != "win" && os != "mac"
+ condprof # bug 1769154 - not realistic for condprof
+support-files =
+ LibraryWithNoData/**
+[test_ChromeMigrationUtils.js]
+[test_ChromeMigrationUtils_path.js]
+[test_Edge_db_migration.js]
+skip-if = os != "win"
+[test_fx_telemetry.js]
+[test_IE_bookmarks.js]
+skip-if = !(os == "win" && bits == 64) # bug 1392396
+[test_IE_cookies.js]
+skip-if = os != "win"
+ (os == "win" && bits == 64 && processor == "x86_64") # bug 1522818
+ (os == "win" && debug && bits == 32) # win10-32/debug
+[test_IE_history.js]
+skip-if =
+ os != "win"
+ os == "win" && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807928
+[test_IE7_passwords.js]
+skip-if = os != "win"
+[test_MigrationUtils_timedRetry.js]
+skip-if = !debug && os == "mac" #Bug 1558330
+[test_Safari_bookmarks.js]
+skip-if = os != "mac"