diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /browser/components/migration/tests | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/migration/tests')
103 files changed, 10456 insertions, 0 deletions
diff --git a/browser/components/migration/tests/browser/browser.toml b/browser/components/migration/tests/browser/browser.toml new file mode 100644 index 0000000000..637c6da345 --- /dev/null +++ b/browser/components/migration/tests/browser/browser.toml @@ -0,0 +1,50 @@ +[DEFAULT] +head = "head.js" +prefs = [ + "browser.migrate.internal-testing.enabled=true", + "dom.window.sizeToContent.enabled=true", +] +support-files = ["../head-common.js"] + +["browser_aboutwelcome_behavior.js"] + +["browser_dialog_cancel_close.js"] + +["browser_dialog_open.js"] + +["browser_dialog_resize.js"] + +["browser_disabled_migrator.js"] + +["browser_do_migration.js"] + +["browser_entrypoint_telemetry.js"] + +["browser_extension_migration.js"] +skip-if = ["win11_2009"] # Bug 1840718 + +["browser_file_migration.js"] +skip-if = [ + "os == 'win' && debug", # Bug 1827995 + "a11y_checks", # Bug 1858037 to investigate intermittent a11y_checks results (fails on Try, passes on Autoland) +] +support-files = ["dummy_file.csv"] + +["browser_ie_edge_bookmarks_success_strings.js"] +skip-if = ["a11y_checks"] # Bug 1858037 to investigate intermittent a11y_checks results (fails on Try, passes on Autoland) + +["browser_misc_telemetry.js"] +skip-if = ["a11y_checks"] # Bug 1858037 to investigate intermittent a11y_checks results (fails on Autoland, passes on Try) + +["browser_no_browsers_state.js"] + +["browser_only_file_migrators.js"] + +["browser_permissions.js"] +skip-if = ["a11y_checks"] # Bug 1858037 and 1855492 to investigate intermittent a11y_checks results (fails on Try, passes on Autoland) + +["browser_safari_passwords.js"] +run-if = ["os == 'mac'"] + +["browser_safari_permissions.js"] +run-if = ["os == 'mac'"] diff --git a/browser/components/migration/tests/browser/browser_aboutwelcome_behavior.js b/browser/components/migration/tests/browser/browser_aboutwelcome_behavior.js new file mode 100644 index 0000000000..72c90851e2 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_aboutwelcome_behavior.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if browser.migrate.content-modal.about-welcome-behavior + * is "autoclose", that closing the migration dialog when opened with the + * NEWTAB entrypoint (which currently only occurs from about:welcome), + * will result in the about:preferences tab closing too. + */ +add_task(async function test_autoclose_from_welcome() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.migrate.content-modal.about-welcome-behavior", "autoclose"], + ], + }); + + let migrationDialogPromise = waitForMigrationWizardDialogTab(); + MigrationUtils.showMigrationWizard(window, { + entrypoint: MigrationUtils.MIGRATION_ENTRYPOINTS.NEWTAB, + }); + + let prefsBrowser = await migrationDialogPromise; + let prefsTab = gBrowser.getTabForBrowser(prefsBrowser); + + let tabClosed = BrowserTestUtils.waitForTabClosing(prefsTab); + + let dialog = prefsBrowser.contentDocument.querySelector( + "#migrationWizardDialog" + ); + + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close"); + await BrowserTestUtils.synthesizeKey("VK_ESCAPE", {}, prefsBrowser); + await dialogClosed; + await tabClosed; + Assert.ok(true, "Preferences tab closed with autoclose behavior."); +}); + +/** + * Tests that if browser.migrate.content-modal.about-welcome-behavior + * is "default", that closing the migration dialog when opened with the + * NEWTAB entrypoint (which currently only occurs from about:welcome), + * will result in the about:preferences tab still staying open. + */ +add_task(async function test_no_autoclose_from_welcome() { + // Create a new blank tab which about:preferences will open into. + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank"); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.migrate.content-modal.about-welcome-behavior", "default"]], + }); + + let migrationDialogPromise = waitForMigrationWizardDialogTab(); + MigrationUtils.showMigrationWizard(window, { + entrypoint: MigrationUtils.MIGRATION_ENTRYPOINTS.NEWTAB, + }); + + let prefsBrowser = await migrationDialogPromise; + let prefsTab = gBrowser.getTabForBrowser(prefsBrowser); + + let dialog = prefsBrowser.contentDocument.querySelector( + "#migrationWizardDialog" + ); + + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close"); + await BrowserTestUtils.synthesizeKey("VK_ESCAPE", {}, prefsBrowser); + await dialogClosed; + Assert.ok(!prefsTab.closing, "about:preferences tab is not closing."); + + BrowserTestUtils.removeTab(prefsTab); +}); + +/** + * Tests that if browser.migrate.content-modal.about-welcome-behavior + * is "standalone", that opening the migration wizard from the NEWTAB + * entrypoint opens the migration wizard in a standalone top-level + * window. + */ +add_task(async function test_no_autoclose_from_welcome() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.migrate.content-modal.about-welcome-behavior", "standalone"], + ], + }); + + let windowOpened = BrowserTestUtils.domWindowOpened(); + MigrationUtils.showMigrationWizard(window, { + entrypoint: MigrationUtils.MIGRATION_ENTRYPOINTS.NEWTAB, + }); + let dialogWin = await windowOpened; + Assert.ok(dialogWin, "Top-level dialog window opened for the migrator."); + await BrowserTestUtils.waitForEvent(dialogWin, "MigrationWizard:Ready"); + + let dialogClosed = BrowserTestUtils.domWindowClosed(dialogWin); + dialogWin.close(); + await dialogClosed; +}); diff --git a/browser/components/migration/tests/browser/browser_dialog_cancel_close.js b/browser/components/migration/tests/browser/browser_dialog_cancel_close.js new file mode 100644 index 0000000000..8fe510cf30 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_dialog_cancel_close.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that pressing "Cancel" from the selection page of the migration + * dialog closes the dialog when opened in about:preferences as an HTML5 + * dialog. + */ +add_task(async function test_cancel_close() { + await withMigrationWizardDialog(async prefsWin => { + let dialog = prefsWin.document.querySelector("#migrationWizardDialog"); + let wizard = dialog.querySelector("migration-wizard"); + let shadow = wizard.openOrClosedShadowRoot; + let cancelButton = shadow.querySelector( + 'div[name="page-selection"] .cancel-close' + ); + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close"); + cancelButton.click(); + await dialogClosed; + Assert.ok(true, "Clicking the cancel button closed the dialog."); + }); +}); + +/** + * Tests that pressing "Cancel" from the selection page of the migration + * dialog closes the dialog when opened in stand-alone window. + */ +add_task(async function test_cancel_close() { + let promiseWinLoaded = BrowserTestUtils.domWindowOpened().then(win => { + return BrowserTestUtils.waitForEvent(win, "MigrationWizard:Ready"); + }); + + let win = Services.ww.openWindow( + window, + DIALOG_URL, + "_blank", + "dialog,centerscreen", + {} + ); + await promiseWinLoaded; + + win.sizeToContent(); + let wizard = win.document.querySelector("#wizard"); + let shadow = wizard.openOrClosedShadowRoot; + let cancelButton = shadow.querySelector( + 'div[name="page-selection"] .cancel-close' + ); + + let windowClosed = BrowserTestUtils.windowClosed(win); + cancelButton.click(); + await windowClosed; + Assert.ok(true, "Window was closed."); +}); diff --git a/browser/components/migration/tests/browser/browser_dialog_open.js b/browser/components/migration/tests/browser/browser_dialog_open.js new file mode 100644 index 0000000000..1ec43f0ea6 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_dialog_open.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that we can open the migration dialog in an about:preferences + * HTML5 dialog when calling MigrationUtils.showMigrationWizard within a + * tabbrowser window execution context. + */ +add_task(async function test_migration_dialog_open_in_tab_dialog_box() { + let migrationDialogPromise = waitForMigrationWizardDialogTab(); + MigrationUtils.showMigrationWizard(window, {}); + let prefsBrowser = await migrationDialogPromise; + Assert.ok(true, "Migration dialog was opened"); + let dialog = prefsBrowser.contentDocument.querySelector( + "#migrationWizardDialog" + ); + + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close"); + await BrowserTestUtils.synthesizeKey("VK_ESCAPE", {}, prefsBrowser); + await dialogClosed; + BrowserTestUtils.startLoadingURIString(prefsBrowser, "about:blank"); + await BrowserTestUtils.browserLoaded(prefsBrowser); +}); + +/** + * Tests that we can open the migration dialog in a stand-alone window + * when calling MigrationUtils.showMigrationWizard with a null opener + * argument, or a non-tabbrowser window context. + */ +add_task(async function test_migration_dialog_open_in_xul_window() { + let firstWindowOpened = BrowserTestUtils.domWindowOpened(); + MigrationUtils.showMigrationWizard(null, {}); + let firstDialogWin = await firstWindowOpened; + + await BrowserTestUtils.waitForEvent(firstDialogWin, "MigrationWizard:Ready"); + + Assert.ok(true, "Migration dialog was opened"); + + // Now open a second migration dialog, using the first as the window + // argument. + + let secondWindowOpened = BrowserTestUtils.domWindowOpened(); + MigrationUtils.showMigrationWizard(firstDialogWin, {}); + let secondDialogWin = await secondWindowOpened; + + await BrowserTestUtils.waitForEvent(secondDialogWin, "MigrationWizard:Ready"); + + for (let dialogWin of [firstDialogWin, secondDialogWin]) { + let dialogClosed = BrowserTestUtils.domWindowClosed(dialogWin); + dialogWin.close(); + await dialogClosed; + } +}); diff --git a/browser/components/migration/tests/browser/browser_dialog_resize.js b/browser/components/migration/tests/browser/browser_dialog_resize.js new file mode 100644 index 0000000000..8fb05faf2c --- /dev/null +++ b/browser/components/migration/tests/browser/browser_dialog_resize.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if the MigrationWizard resizes when opened inside of a + * XUL window, that it causes the containing XUL window to resize + * appropriately. + */ +add_task(async function test_migration_dialog_resize_in_xul_window() { + let windowOpened = BrowserTestUtils.domWindowOpened(); + MigrationUtils.showMigrationWizard(null, {}); + let dialogWin = await windowOpened; + + await BrowserTestUtils.waitForEvent(dialogWin, "MigrationWizard:Ready"); + + let wizard = dialogWin.document.body.querySelector("#wizard"); + let height = wizard.getBoundingClientRect().height; + + let windowResizePromise = BrowserTestUtils.waitForEvent(dialogWin, "resize"); + wizard.style.height = height + 100 + "px"; + await windowResizePromise; + Assert.ok(true, "Migration dialog window resized."); + + let dialogClosed = BrowserTestUtils.domWindowClosed(dialogWin); + dialogWin.close(); + await dialogClosed; +}); diff --git a/browser/components/migration/tests/browser/browser_disabled_migrator.js b/browser/components/migration/tests/browser/browser_disabled_migrator.js new file mode 100644 index 0000000000..782666f6a6 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_disabled_migrator.js @@ -0,0 +1,131 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { MigratorBase } = ChromeUtils.importESModule( + "resource:///modules/MigratorBase.sys.mjs" +); + +/** + * Tests that the InternalTestingProfileMigrator is listed in + * the new migration wizard selector when enabled. + */ +add_task(async function test_enabled_migrator() { + await withMigrationWizardDialog(async prefsWin => { + let dialog = prefsWin.document.querySelector("#migrationWizardDialog"); + let wizard = dialog.querySelector("migration-wizard"); + let shadow = wizard.openOrClosedShadowRoot; + let selector = shadow.querySelector("#browser-profile-selector"); + selector.click(); + + await new Promise(resolve => { + shadow + .querySelector("panel-list") + .addEventListener("shown", resolve, { once: true }); + }); + + let panelItem = shadow.querySelector( + `panel-item[key="${InternalTestingProfileMigrator.key}"]` + ); + + Assert.ok( + panelItem, + "The InternalTestingProfileMigrator panel-item exists." + ); + panelItem.click(); + + Assert.ok( + selector.innerText.includes("Internal Testing Migrator"), + "Testing for enabled internal testing migrator" + ); + }); +}); + +/** + * Tests that the InternalTestingProfileMigrator is not listed in + * the new migration wizard selector when disabled. + */ +add_task(async function test_disabling_migrator() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.migrate.internal-testing.enabled", false]], + }); + + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + let internalTestingMigrator = new InternalTestingProfileMigrator(); + + // We create a fake migrator that we know will still be present after + // disabling the InternalTestingProfileMigrator so that we don't switch + // the wizard to the NO_BROWSERS_FOUND page, which we're not testing here. + let fakeMigrator = new FakeMigrator(); + + let getMigratorStub = sandbox.stub(MigrationUtils, "getMigrator"); + getMigratorStub + .withArgs("internal-testing") + .resolves(internalTestingMigrator); + getMigratorStub.withArgs("fake-migrator").resolves(fakeMigrator); + + sandbox.stub(MigrationUtils, "availableMigratorKeys").get(() => { + return ["internal-testing", "fake-migrator"]; + }); + + await withMigrationWizardDialog(async prefsWin => { + let dialog = prefsWin.document.querySelector("#migrationWizardDialog"); + let wizard = dialog.querySelector("migration-wizard"); + let shadow = wizard.openOrClosedShadowRoot; + let selector = shadow.querySelector("#browser-profile-selector"); + selector.click(); + + await new Promise(resolve => { + shadow + .querySelector("panel-list") + .addEventListener("shown", resolve, { once: true }); + }); + + let panelItem = shadow.querySelector( + `panel-item[key="${InternalTestingProfileMigrator.key}"]` + ); + + Assert.ok( + !panelItem, + "The panel-item for the InternalTestingProfileMigrator does not exist" + ); + }); + + sandbox.restore(); +}); + +/** + * A stub of a migrator used for automated testing only. + */ +class FakeMigrator extends MigratorBase { + static get key() { + return "fake-migrator"; + } + + static get displayNameL10nID() { + return "migration-wizard-migrator-display-name-firefox"; + } + + // We will create a single MigratorResource for each resource type that + // just immediately reports a successful migration. + getResources() { + return Object.values(MigrationUtils.resourceTypes).map(type => { + return { + type, + migrate: callback => { + callback(true /* success */); + }, + }; + }); + } + + // We need to override enabled() to always return true for testing purposes. + get enabled() { + return true; + } +} diff --git a/browser/components/migration/tests/browser/browser_do_migration.js b/browser/components/migration/tests/browser/browser_do_migration.js new file mode 100644 index 0000000000..fab9641960 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_do_migration.js @@ -0,0 +1,209 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the MigrationWizard can be used to successfully migrate + * using the InternalTestingProfileMigrator in a few scenarios. + */ +add_task(async function test_successful_migrations() { + // Scenario 1: A single resource type is available. + let migration = waitForTestMigration( + [MigrationUtils.resourceTypes.BOOKMARKS], + [MigrationUtils.resourceTypes.BOOKMARKS], + InternalTestingProfileMigrator.testProfile + ); + + await withMigrationWizardDialog(async prefsWin => { + let dialogBody = prefsWin.document.body; + let wizard = dialogBody.querySelector("migration-wizard"); + let shadow = wizard.openOrClosedShadowRoot; + let selector = shadow.querySelector("#browser-profile-selector"); + + await new Promise(resolve => prefsWin.requestAnimationFrame(resolve)); + Assert.equal(shadow.activeElement, selector, "Selector should be focused."); + + let wizardDone = BrowserTestUtils.waitForEvent( + wizard, + "MigrationWizard:DoneMigration" + ); + selectResourceTypesAndStartMigration(wizard, [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS, + ]); + await migration; + await wizardDone; + + let dialog = prefsWin.document.querySelector("#migrationWizardDialog"); + let doneButton = shadow.querySelector( + "div[name='page-progress'] .done-button" + ); + + await new Promise(resolve => prefsWin.requestAnimationFrame(resolve)); + Assert.equal( + shadow.activeElement, + doneButton, + "Done button should be focused." + ); + + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close"); + doneButton.click(); + await dialogClosed; + assertQuantitiesShown(wizard, [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS, + ]); + }); + + // We should make sure that the migration.time_to_produce_migrator_list + // scalar was set, since we know that at least one migration wizard has + // been opened. + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + Assert.ok( + scalars["migration.time_to_produce_migrator_list"] > 0, + "Non-zero scalar value recorded for migration.time_to_produce_migrator_list" + ); + + // Scenario 2: Several resource types are available, but only 1 + // is checked / expected. + migration = waitForTestMigration( + [ + MigrationUtils.resourceTypes.BOOKMARKS, + MigrationUtils.resourceTypes.PASSWORDS, + ], + [MigrationUtils.resourceTypes.PASSWORDS], + InternalTestingProfileMigrator.testProfile + ); + + await withMigrationWizardDialog(async prefsWin => { + let dialogBody = prefsWin.document.body; + let wizard = dialogBody.querySelector("migration-wizard"); + let shadow = wizard.openOrClosedShadowRoot; + let selector = shadow.querySelector("#browser-profile-selector"); + + await new Promise(resolve => prefsWin.requestAnimationFrame(resolve)); + Assert.equal(shadow.activeElement, selector, "Selector should be focused."); + + let wizardDone = BrowserTestUtils.waitForEvent( + wizard, + "MigrationWizard:DoneMigration" + ); + selectResourceTypesAndStartMigration(wizard, [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS, + ]); + await migration; + await wizardDone; + + let dialog = prefsWin.document.querySelector("#migrationWizardDialog"); + let doneButton = shadow.querySelector( + "div[name='page-progress'] .done-button" + ); + + await new Promise(resolve => prefsWin.requestAnimationFrame(resolve)); + Assert.equal( + shadow.activeElement, + doneButton, + "Done button should be focused." + ); + + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close"); + doneButton.click(); + await dialogClosed; + assertQuantitiesShown(wizard, [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS, + ]); + }); + + // Scenario 3: Several resource types are available, all are checked. + let allResourceTypeStrs = Object.values( + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES + ).filter(resourceStr => { + return !MigrationWizardConstants.PROFILE_RESET_ONLY_RESOURCE_TYPES[ + resourceStr + ]; + }); + + let allResourceTypes = allResourceTypeStrs.map(resourceTypeStr => { + return MigrationUtils.resourceTypes[resourceTypeStr]; + }); + + migration = waitForTestMigration( + allResourceTypes, + allResourceTypes, + InternalTestingProfileMigrator.testProfile + ); + + await withMigrationWizardDialog(async prefsWin => { + let dialogBody = prefsWin.document.body; + let wizard = dialogBody.querySelector("migration-wizard"); + let shadow = wizard.openOrClosedShadowRoot; + let selector = shadow.querySelector("#browser-profile-selector"); + + await new Promise(resolve => prefsWin.requestAnimationFrame(resolve)); + Assert.equal(shadow.activeElement, selector, "Selector should be focused."); + + let wizardDone = BrowserTestUtils.waitForEvent( + wizard, + "MigrationWizard:DoneMigration" + ); + selectResourceTypesAndStartMigration(wizard, allResourceTypeStrs); + await migration; + await wizardDone; + assertQuantitiesShown(wizard, allResourceTypeStrs); + }); +}); + +/** + * Tests that if somehow the Migration Wizard requests to import a + * resource type that the migrator doesn't have the ability to import, + * that it's ignored and the migration completes normally. + */ +add_task(async function test_invalid_resource_type() { + let migration = waitForTestMigration( + [MigrationUtils.resourceTypes.BOOKMARKS], + [MigrationUtils.resourceTypes.BOOKMARKS], + InternalTestingProfileMigrator.testProfile + ); + + await withMigrationWizardDialog(async prefsWin => { + let dialogBody = prefsWin.document.body; + let wizard = dialogBody.querySelector("migration-wizard"); + let wizardDone = BrowserTestUtils.waitForEvent( + wizard, + "MigrationWizard:DoneMigration" + ); + + // The Migration Wizard _shouldn't_ display anything except BOOKMARKS, + // since that's the only resource type that the selected migrator is + // supposed to currently support, but we'll check the other checkboxes + // even though they're hidden just to see what happens. + selectResourceTypesAndStartMigration(wizard, [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS, + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS, + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY, + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA, + ]); + await migration; + await wizardDone; + + let dialog = prefsWin.document.querySelector("#migrationWizardDialog"); + let shadow = wizard.openOrClosedShadowRoot; + let doneButton = shadow.querySelector( + "div[name='page-progress'] .done-button" + ); + + await new Promise(resolve => prefsWin.requestAnimationFrame(resolve)); + Assert.equal( + shadow.activeElement, + doneButton, + "Done button should be focused." + ); + + assertQuantitiesShown(wizard, [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS, + ]); + + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close"); + doneButton.click(); + await dialogClosed; + }); +}); diff --git a/browser/components/migration/tests/browser/browser_entrypoint_telemetry.js b/browser/components/migration/tests/browser/browser_entrypoint_telemetry.js new file mode 100644 index 0000000000..a1bb23d7fc --- /dev/null +++ b/browser/components/migration/tests/browser/browser_entrypoint_telemetry.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const HISTOGRAM_ID = "FX_MIGRATION_ENTRY_POINT_CATEGORICAL"; + +async function showThenCloseMigrationWizardViaEntrypoint(entrypoint) { + let openedPromise = BrowserTestUtils.waitForMigrationWizard(window); + + MigrationUtils.showMigrationWizard(window, { + entrypoint, + }); + + let wizardTab = await openedPromise; + Assert.ok(wizardTab, "Migration wizard opened."); + + await BrowserTestUtils.removeTab(wizardTab); +} + +add_setup(async () => { + // Load the initial tab at example.com. This makes it so that if + // when we load the wizard in about:preferences, we'll load the + // about:preferences page in a new tab rather than overtaking the + // initial one. This makes cleanup of the wizard more explicit, since + // we can just close the tab. + let browser = gBrowser.selectedBrowser; + BrowserTestUtils.startLoadingURIString(browser, "https://example.com"); + await BrowserTestUtils.browserLoaded(browser); +}); + +/** + * Tests that the entrypoint passed to MigrationUtils.showMigrationWizard gets + * written to both the FX_MIGRATION_ENTRY_POINT_CATEGORICAL histogram. + */ +add_task(async function test_entrypoints() { + let histogram = TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_ID); + + // Let's arbitrarily pick the "Bookmarks" entrypoint, and make sure this + // is recorded. + await showThenCloseMigrationWizardViaEntrypoint( + MigrationUtils.MIGRATION_ENTRYPOINTS.BOOKMARKS + ); + let entrypointIndex = getEntrypointHistogramIndex( + MigrationUtils.MIGRATION_ENTRYPOINTS.BOOKMARKS + ); + + TelemetryTestUtils.assertHistogram(histogram, entrypointIndex, 1); + + histogram = TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_ID); + + // Now let's pick the "Preferences" entrypoint, and make sure this + // is recorded. + await showThenCloseMigrationWizardViaEntrypoint( + MigrationUtils.MIGRATION_ENTRYPOINTS.PREFERENCES + ); + entrypointIndex = getEntrypointHistogramIndex( + MigrationUtils.MIGRATION_ENTRYPOINTS.PREFERENCES + ); + + TelemetryTestUtils.assertHistogram(histogram, entrypointIndex, 1); + + histogram = TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_ID); + + // Finally, check the fallback by passing in something invalid as an entrypoint. + await showThenCloseMigrationWizardViaEntrypoint(undefined); + entrypointIndex = getEntrypointHistogramIndex( + MigrationUtils.MIGRATION_ENTRYPOINTS.UNKNOWN + ); + + TelemetryTestUtils.assertHistogram(histogram, entrypointIndex, 1); +}); diff --git a/browser/components/migration/tests/browser/browser_extension_migration.js b/browser/components/migration/tests/browser/browser_extension_migration.js new file mode 100644 index 0000000000..e9c3c65e6d --- /dev/null +++ b/browser/components/migration/tests/browser/browser_extension_migration.js @@ -0,0 +1,238 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let gFluentStrings = new Localization([ + "branding/brand.ftl", + "browser/migrationWizard.ftl", +]); + +/** + * Ensures that the wizard is on the progress page and that the extension + * resource group matches a particular state. + * + * @param {Element} wizard + * The <migration-wizard> element to inspect. + * @param {number} state + * One of the constants from MigrationWizardConstants.PROGRESS_VALUE, + * describing what state the resource group should be in. + * @param {object} description + * An object to express more details of how the resource group should be + * displayed. + * @param {string} description.message + * The message that should be displayed for the resource group. This message + * maybe be contained in different elements depending on the state. + * @param {string} description.linkURL + * The URL for the <a> element that should be displayed to the user for the + * particular state. + * @param {string} description.linkText + * The text content for the <a> element that should be displayed to the user + * for the particular state. + * @returns {Promise<undefined>} + */ +async function assertExtensionsProgressState(wizard, state, description) { + let shadow = wizard.openOrClosedShadowRoot; + + // Make sure that we're showing the progress page first. + let deck = shadow.querySelector("#wizard-deck"); + Assert.equal( + deck.selectedViewName, + `page-${MigrationWizardConstants.PAGES.PROGRESS}` + ); + + let progressGroup = shadow.querySelector( + `.resource-progress-group[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS}"` + ); + + let progressIcon = progressGroup.querySelector(".progress-icon"); + let messageText = progressGroup.querySelector("span.message-text"); + let supportLink = progressGroup.querySelector(".support-text"); + let extensionsSuccessLink = progressGroup.querySelector( + "#extensions-success-link" + ); + + if (state == MigrationWizardConstants.PROGRESS_VALUE.SUCCESS) { + Assert.stringMatches(progressIcon.getAttribute("state"), "success"); + Assert.stringMatches(messageText.textContent, ""); + Assert.stringMatches(supportLink.textContent, ""); + await assertSuccessLink(extensionsSuccessLink, description.message); + } else if (state == MigrationWizardConstants.PROGRESS_VALUE.WARNING) { + Assert.stringMatches(progressIcon.getAttribute("state"), "warning"); + Assert.stringMatches(messageText.textContent, description.message); + Assert.stringMatches(supportLink.textContent, description.linkText); + Assert.stringMatches(supportLink.href, description.linkURL); + await assertSuccessLink(extensionsSuccessLink, ""); + } else if (state == MigrationWizardConstants.PROGRESS_VALUE.INFO) { + Assert.stringMatches(progressIcon.getAttribute("state"), "info"); + Assert.stringMatches(supportLink.textContent, ""); + await assertSuccessLink(extensionsSuccessLink, description.message); + } +} + +/** + * Checks that the extensions migration success link has the right + * text content, and if the text content is non-blank, ensures that + * clicking on the link opens up about:addons in a background tab. + * + * The about:addons tab will be automatically closed before proceeding. + * + * @param {Element} link + * The extensions migration success link element. + * @param {string} message + * The expected string to appear in the link textContent. If the + * link is not expected to appear, this should be the empty string. + * @returns {Promise<undefined>} + */ +async function assertSuccessLink(link, message) { + Assert.stringMatches(link.textContent, message); + if (message) { + let aboutAddonsOpened = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:addons" + ); + EventUtils.synthesizeMouseAtCenter(link, {}, link.ownerGlobal); + let tab = await aboutAddonsOpened; + BrowserTestUtils.removeTab(tab); + } +} + +/** + * Checks the case where no extensions were matched. + */ +add_task(async function test_extension_migration_no_matched_extensions() { + let migration = waitForTestMigration( + [MigrationUtils.resourceTypes.EXTENSIONS], + [MigrationUtils.resourceTypes.EXTENSIONS], + InternalTestingProfileMigrator.testProfile, + [MigrationUtils.resourceTypes.EXTENSIONS], + 3 /* totalExtensions */, + 0 /* matchedExtensions */ + ); + + await withMigrationWizardDialog(async prefsWin => { + let dialogBody = prefsWin.document.body; + let wizard = dialogBody.querySelector("migration-wizard"); + + let wizardDone = BrowserTestUtils.waitForEvent( + wizard, + "MigrationWizard:DoneMigration" + ); + selectResourceTypesAndStartMigration(wizard, [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS, + ]); + await migration; + await wizardDone; + await assertExtensionsProgressState( + wizard, + MigrationWizardConstants.PROGRESS_VALUE.WARNING, + { + message: await gFluentStrings.formatValue( + "migration-wizard-progress-no-matched-extensions" + ), + linkURL: Services.urlFormatter.formatURLPref( + "extensions.getAddons.link.url" + ), + linkText: await gFluentStrings.formatValue( + "migration-wizard-progress-extensions-addons-link" + ), + } + ); + }); +}); + +/** + * Checks the case where some but not all extensions were matched. + */ +add_task( + async function test_extension_migration_partially_matched_extensions() { + const TOTAL_EXTENSIONS = 3; + const TOTAL_MATCHES = 1; + + let migration = waitForTestMigration( + [MigrationUtils.resourceTypes.EXTENSIONS], + [MigrationUtils.resourceTypes.EXTENSIONS], + InternalTestingProfileMigrator.testProfile, + [], + TOTAL_EXTENSIONS, + TOTAL_MATCHES + ); + + await withMigrationWizardDialog(async prefsWin => { + let dialogBody = prefsWin.document.body; + let wizard = dialogBody.querySelector("migration-wizard"); + + let wizardDone = BrowserTestUtils.waitForEvent( + wizard, + "MigrationWizard:DoneMigration" + ); + selectResourceTypesAndStartMigration(wizard, [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS, + ]); + await migration; + await wizardDone; + await assertExtensionsProgressState( + wizard, + MigrationWizardConstants.PROGRESS_VALUE.INFO, + { + message: await gFluentStrings.formatValue( + "migration-wizard-progress-partial-success-extensions", + { + matched: TOTAL_MATCHES, + quantity: TOTAL_EXTENSIONS, + } + ), + linkText: await gFluentStrings.formatValue( + "migration-wizard-progress-extensions-support-link" + ), + } + ); + }); + } +); + +/** + * Checks the case where all extensions were matched. + */ +add_task(async function test_extension_migration_fully_matched_extensions() { + const TOTAL_EXTENSIONS = 15; + const TOTAL_MATCHES = TOTAL_EXTENSIONS; + + let migration = waitForTestMigration( + [MigrationUtils.resourceTypes.EXTENSIONS], + [MigrationUtils.resourceTypes.EXTENSIONS], + InternalTestingProfileMigrator.testProfile, + [], + TOTAL_EXTENSIONS, + TOTAL_MATCHES + ); + + await withMigrationWizardDialog(async prefsWin => { + let dialogBody = prefsWin.document.body; + let wizard = dialogBody.querySelector("migration-wizard"); + + let wizardDone = BrowserTestUtils.waitForEvent( + wizard, + "MigrationWizard:DoneMigration" + ); + selectResourceTypesAndStartMigration(wizard, [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS, + ]); + await migration; + await wizardDone; + await assertExtensionsProgressState( + wizard, + MigrationWizardConstants.PROGRESS_VALUE.SUCCESS, + { + message: await gFluentStrings.formatValue( + "migration-wizard-progress-success-extensions", + { + quantity: TOTAL_EXTENSIONS, + } + ), + linkURL: "", + linkText: "", + } + ); + }); +}); diff --git a/browser/components/migration/tests/browser/browser_file_migration.js b/browser/components/migration/tests/browser/browser_file_migration.js new file mode 100644 index 0000000000..04241d29d5 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_file_migration.js @@ -0,0 +1,306 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { FileMigratorBase } = ChromeUtils.importESModule( + "resource:///modules/FileMigrators.sys.mjs" +); + +const DUMMY_FILEMIGRATOR_KEY = "dummy-file-migrator"; +const DUMMY_FILEPICKER_TITLE = "Some dummy file picker title"; +const DUMMY_FILTER_TITLE = "Some file type"; +const DUMMY_EXTENSION_PATTERN = "*.test"; +const TEST_FILE_PATH = getTestFilePath("dummy_file.csv"); + +/** + * A subclass of FileMigratorBase that doesn't do anything, but + * is useful for testing. + * + * Notably, the `migrate` method is not overridden here. Tests that + * use this class should use Sinon to stub out the migrate method. + */ +class DummyFileMigrator extends FileMigratorBase { + static get key() { + return DUMMY_FILEMIGRATOR_KEY; + } + + static get displayNameL10nID() { + return "migration-wizard-migrator-display-name-file-password-csv"; + } + + static get brandImage() { + return "chrome://branding/content/document.ico"; + } + + get enabled() { + return true; + } + + get progressHeaderL10nID() { + return "migration-passwords-from-file-progress-header"; + } + + get successHeaderL10nID() { + return "migration-passwords-from-file-success-header"; + } + + async getFilePickerConfig() { + return Promise.resolve({ + title: DUMMY_FILEPICKER_TITLE, + filters: [ + { + title: DUMMY_FILTER_TITLE, + extensionPattern: DUMMY_EXTENSION_PATTERN, + }, + ], + }); + } + + get displayedResourceTypes() { + return [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS]; + } +} + +const { MockFilePicker } = SpecialPowers; + +add_setup(async () => { + // We use MockFilePicker to simulate a native file picker, and prepare it + // to return a dummy file pointed at TEST_FILE_PATH. The file at + // TEST_FILE_PATH is not required (nor expected) to exist. + MockFilePicker.init(window); + registerCleanupFunction(() => { + MockFilePicker.cleanup(); + }); +}); + +/** + * Tests the flow of selecting a file migrator (in this case, + * the DummyFileMigrator), getting the file picker opened for it, + * and then passing the path of the selected file to the migrator. + */ +add_task(async function test_file_migration() { + let migrator = new DummyFileMigrator(); + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + // First, use Sinon to insert our DummyFileMigrator as the only available + // file migrator. + sandbox.stub(MigrationUtils, "getFileMigrator").callsFake(() => { + return migrator; + }); + sandbox.stub(MigrationUtils, "availableFileMigrators").get(() => { + return [migrator]; + }); + + // This is the expected success state that our DummyFileMigrator will + // return as the final progress update to the migration wizard. + const SUCCESS_STATE = { + [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW]: + "2 added", + [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_UPDATED]: + "1 updated", + }; + + let migrateStub = sandbox.stub(migrator, "migrate").callsFake(filePath => { + Assert.equal(filePath, TEST_FILE_PATH); + return SUCCESS_STATE; + }); + + let dummyFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + dummyFile.initWithPath(TEST_FILE_PATH); + let filePickerShownPromise = new Promise(resolve => { + MockFilePicker.showCallback = () => { + Assert.ok(true, "Filepicker shown."); + MockFilePicker.setFiles([dummyFile]); + resolve(); + }; + }); + MockFilePicker.returnValue = MockFilePicker.returnOK; + + await withMigrationWizardDialog(async prefsWin => { + let dialogBody = prefsWin.document.body; + let wizard = dialogBody.querySelector("migration-wizard"); + let shadow = wizard.openOrClosedShadowRoot; + + let wizardDone = BrowserTestUtils.waitForEvent( + wizard, + "MigrationWizard:DoneMigration" + ); + + // Now select our DummyFileMigrator from the list. + let selector = shadow.querySelector("#browser-profile-selector"); + selector.click(); + + info("Waiting for panel-list shown"); + await new Promise(resolve => { + shadow + .querySelector("panel-list") + .addEventListener("shown", resolve, { once: true }); + }); + + info("Panel list shown. Clicking on panel-item"); + let panelItem = shadow.querySelector( + `panel-item[key="${DUMMY_FILEMIGRATOR_KEY}"]` + ); + panelItem.click(); + + // Selecting a file migrator from the selector should automatically + // open the file picker, so we await it here. Once the file is + // selected, migration should begin immediately. + + info("Waiting for file picker"); + await filePickerShownPromise; + await wizardDone; + Assert.ok(migrateStub.called, "Migrate on DummyFileMigrator was called."); + + // At this point, with migration having completed, we should be showing + // the PROGRESS page with the SUCCESS_STATE represented. + let deck = shadow.querySelector("#wizard-deck"); + Assert.equal( + deck.selectedViewName, + `page-${MigrationWizardConstants.PAGES.FILE_IMPORT_PROGRESS}` + ); + + // We expect only the displayed resource types in SUCCESS_STATE are + // displayed now. + let progressGroups = shadow.querySelectorAll( + "div[name='page-file-import-progress'] .resource-progress-group" + ); + for (let progressGroup of progressGroups) { + let expectedMessageText = + SUCCESS_STATE[progressGroup.dataset.resourceType]; + if (expectedMessageText) { + let progressIcon = progressGroup.querySelector(".progress-icon"); + Assert.stringMatches( + progressIcon.getAttribute("state"), + "success", + "Should be showing completed state." + ); + + let messageText = + progressGroup.querySelector(".message-text").textContent; + Assert.equal(messageText, expectedMessageText); + } else { + Assert.ok( + BrowserTestUtils.isHidden(progressGroup), + `Resource progress group for ${progressGroup.dataset.resourceType}` + + ` should be hidden.` + ); + } + } + }); + + sandbox.restore(); +}); + +/** + * Tests that the migration wizard will go back to the selection page and + * show an error message if the migration for a FileMigrator throws an + * exception. + */ +add_task(async function test_file_migration_error() { + let migrator = new DummyFileMigrator(); + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + // First, use Sinon to insert our DummyFileMigrator as the only available + // file migrator. + sandbox.stub(MigrationUtils, "getFileMigrator").callsFake(() => { + return migrator; + }); + sandbox.stub(MigrationUtils, "availableFileMigrators").get(() => { + return [migrator]; + }); + + const ERROR_MESSAGE = "This is my error message"; + + let migrateStub = sandbox.stub(migrator, "migrate").callsFake(() => { + throw new Error(ERROR_MESSAGE); + }); + + let dummyFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + dummyFile.initWithPath(TEST_FILE_PATH); + let filePickerShownPromise = new Promise(resolve => { + MockFilePicker.showCallback = () => { + Assert.ok(true, "Filepicker shown."); + MockFilePicker.setFiles([dummyFile]); + resolve(); + }; + }); + MockFilePicker.returnValue = MockFilePicker.returnOK; + + await withMigrationWizardDialog(async prefsWin => { + let dialogBody = prefsWin.document.body; + let wizard = dialogBody.querySelector("migration-wizard"); + let shadow = wizard.openOrClosedShadowRoot; + + let wizardDone = BrowserTestUtils.waitForEvent( + wizard, + "MigrationWizard:DoneMigration" + ); + + // Now select our DummyFileMigrator from the list. + let selector = shadow.querySelector("#browser-profile-selector"); + selector.click(); + + info("Waiting for panel-list shown"); + await new Promise(resolve => { + shadow + .querySelector("panel-list") + .addEventListener("shown", resolve, { once: true }); + }); + + info("Panel list shown. Clicking on panel-item"); + let panelItem = shadow.querySelector( + `panel-item[key="${DUMMY_FILEMIGRATOR_KEY}"]` + ); + panelItem.click(); + + // Selecting a file migrator from the selector should automatically + // open the file picker, so we await it here. Once the file is + // selected, migration should begin immediately. + + info("Waiting for file picker"); + await filePickerShownPromise; + await wizardDone; + Assert.ok(migrateStub.called, "Migrate on DummyFileMigrator was called."); + + // At this point, with migration having completed, we should be showing + // the SELECTION page again with the ERROR_MESSAGE displayed. + let deck = shadow.querySelector("#wizard-deck"); + await BrowserTestUtils.waitForMutationCondition( + deck, + { attributeFilter: ["selected-view"] }, + () => { + return ( + deck.getAttribute("selected-view") == + "page-" + MigrationWizardConstants.PAGES.SELECTION + ); + } + ); + + Assert.equal( + selector.selectedPanelItem.getAttribute("key"), + DUMMY_FILEMIGRATOR_KEY, + "Should have the file migrator selected." + ); + + let errorMessageContainer = shadow.querySelector(".file-import-error"); + Assert.ok( + BrowserTestUtils.isVisible(errorMessageContainer), + "Should be showing the error message container" + ); + + let fileImportErrorMessage = shadow.querySelector( + "#file-import-error-message" + ).textContent; + Assert.equal(fileImportErrorMessage, ERROR_MESSAGE); + }); + + sandbox.restore(); +}); diff --git a/browser/components/migration/tests/browser/browser_ie_edge_bookmarks_success_strings.js b/browser/components/migration/tests/browser/browser_ie_edge_bookmarks_success_strings.js new file mode 100644 index 0000000000..34e8a8de2e --- /dev/null +++ b/browser/components/migration/tests/browser/browser_ie_edge_bookmarks_success_strings.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the progress strings that the Migration Wizard shows + * during migrations for IE and Edge uses the term "Favorites" rather + * then "Bookmarks". + */ +add_task(async function test_ie_edge_bookmarks_success_strings() { + for (let key of ["ie", "edge", "internal-testing"]) { + let sandbox = sinon.createSandbox(); + + sandbox.stub(InternalTestingProfileMigrator, "key").get(() => { + return key; + }); + + sandbox.stub(MigrationUtils, "availableMigratorKeys").get(() => { + return key; + }); + + let testingMigrator = new InternalTestingProfileMigrator(); + sandbox.stub(MigrationUtils, "getMigrator").callsFake(() => { + return Promise.resolve(testingMigrator); + }); + + let migration = waitForTestMigration( + [MigrationUtils.resourceTypes.BOOKMARKS], + [MigrationUtils.resourceTypes.BOOKMARKS], + InternalTestingProfileMigrator.testProfile + ); + + await withMigrationWizardDialog(async prefsWin => { + let dialogBody = prefsWin.document.body; + let wizard = dialogBody.querySelector("migration-wizard"); + let wizardDone = BrowserTestUtils.waitForEvent( + wizard, + "MigrationWizard:DoneMigration" + ); + selectResourceTypesAndStartMigration( + wizard, + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS], + key + ); + await migration; + + let dialog = prefsWin.document.querySelector("#migrationWizardDialog"); + let shadow = wizard.openOrClosedShadowRoot; + + // If we were using IE or Edge (EdgeHTLM), then the success message should + // include the word "favorites". Otherwise, we expect it to include + // the word "bookmarks". + let bookmarksProgressGroup = shadow.querySelector( + `.resource-progress-group[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS}"` + ); + let messageTextElement = + bookmarksProgressGroup.querySelector(".message-text"); + + await BrowserTestUtils.waitForCondition(() => { + return messageTextElement.textContent.trim(); + }); + + let messageText = messageTextElement.textContent.toLowerCase(); + + if (key == "internal-testing") { + Assert.ok( + messageText.includes("bookmarks"), + `Message text should refer to bookmarks: ${messageText}.` + ); + } else { + Assert.ok( + messageText.includes("favorites"), + `Message text should refer to favorites: ${messageText}` + ); + } + + let doneButton = shadow.querySelector( + "div[name='page-progress'] .done-button" + ); + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close"); + doneButton.click(); + await dialogClosed; + await wizardDone; + }); + + sandbox.restore(); + } +}); diff --git a/browser/components/migration/tests/browser/browser_misc_telemetry.js b/browser/components/migration/tests/browser/browser_misc_telemetry.js new file mode 100644 index 0000000000..4fc6518e49 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_misc_telemetry.js @@ -0,0 +1,178 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ChromeProfileMigrator } = ChromeUtils.importESModule( + "resource:///modules/ChromeProfileMigrator.sys.mjs" +); + +/** + * Tests that if the migration wizard is opened when the + * MOZ_UNINSTALLER_PROFILE_REFRESH environment variable is defined, + * that the migration.uninstaller_profile_refresh scalar is set, + * and the environment variable is cleared. + */ +add_task(async function test_uninstaller_migration() { + if (AppConstants.platform != "win") { + return; + } + + Services.env.set("MOZ_UNINSTALLER_PROFILE_REFRESH", "1"); + let wizardPromise = BrowserTestUtils.domWindowOpened(); + // Opening the migration wizard this way is a blocking function, so + // we delegate it to a runnable. + executeSoon(() => { + MigrationUtils.showMigrationWizard(null, { isStartupMigration: true }); + }); + let wizardWin = await wizardPromise; + + await BrowserTestUtils.waitForEvent(wizardWin, "MigrationWizard:Ready"); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar( + scalars, + "migration.uninstaller_profile_refresh", + 1 + ); + + Assert.equal( + Services.env.get("MOZ_UNINSTALLER_PROFILE_REFRESH"), + "", + "Cleared MOZ_UNINSTALLER_PROFILE_REFRESH environment variable." + ); + await BrowserTestUtils.closeWindow(wizardWin); +}); + +/** + * Tests that we populate the migration.discovered_migrators keyed scalar + * with a count of discovered browsers and profiles. + */ +add_task(async function test_discovered_migrators_keyed_scalar() { + Services.telemetry.clearScalars(); + + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + // We'll pretend that this system only has the + // InternalTestingProfileMigrator and ChromeProfileMigrator around to + // start + sandbox.stub(MigrationUtils, "availableMigratorKeys").get(() => { + return ["internal-testing", "chrome"]; + }); + + // The InternalTestingProfileMigrator by default returns a single profile + // from `getSourceProfiles`, and now we'll just prepare the + // ChromeProfileMigrator to return two fake profiles. + sandbox.stub(ChromeProfileMigrator.prototype, "getSourceProfiles").resolves([ + { id: "chrome-test-1", name: "Chrome test profile 1" }, + { id: "chrome-test-2", name: "Chrome test profile 2" }, + ]); + + sandbox + .stub(ChromeProfileMigrator.prototype, "hasPermissions") + .resolves(true); + + // We also need to ensure that the ChromeProfileMigrator actually has + // some resources to migrate, otherwise it won't get listed. + sandbox + .stub(ChromeProfileMigrator.prototype, "getResources") + .callsFake(() => { + return Promise.resolve( + Object.values(MigrationUtils.resourceType).map(resourceType => { + return { + type: resourceType, + migrate: () => {}, + }; + }) + ); + }); + + await withMigrationWizardDialog(async () => { + let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "migration.discovered_migrators", + InternalTestingProfileMigrator.key, + 1 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "migration.discovered_migrators", + ChromeProfileMigrator.key, + 2 + ); + }); + + // Now, reset, and we'll try the case where a migrator returns `null` from + // `getSourceProfiles` using the InternalTestingProfileMigrator again. + sandbox.restore(); + + sandbox = sinon.createSandbox(); + + sandbox.stub(MigrationUtils, "availableMigratorKeys").get(() => { + return ["internal-testing"]; + }); + + sandbox + .stub(ChromeProfileMigrator.prototype, "getSourceProfiles") + .resolves(null); + + await withMigrationWizardDialog(async () => { + let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "migration.discovered_migrators", + InternalTestingProfileMigrator.key, + 1 + ); + }); + + sandbox.restore(); +}); + +/** + * Tests that we write to the FX_MIGRATION_ERRORS histogram when a + * resource fails to migrate properly. + */ +add_task(async function test_fx_migration_errors() { + let migration = waitForTestMigration( + [ + MigrationUtils.resourceTypes.BOOKMARKS, + MigrationUtils.resourceTypes.PASSWORDS, + ], + [ + MigrationUtils.resourceTypes.BOOKMARKS, + MigrationUtils.resourceTypes.PASSWORDS, + ], + InternalTestingProfileMigrator.testProfile, + [MigrationUtils.resourceTypes.PASSWORDS] + ); + + await withMigrationWizardDialog(async prefsWin => { + let dialogBody = prefsWin.document.body; + let wizard = dialogBody.querySelector("migration-wizard"); + let wizardDone = BrowserTestUtils.waitForEvent( + wizard, + "MigrationWizard:DoneMigration" + ); + + selectResourceTypesAndStartMigration(wizard, [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS, + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS, + ]); + await migration; + await wizardDone; + + assertQuantitiesShown( + wizard, + [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS, + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS, + ], + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS] + ); + }); +}); diff --git a/browser/components/migration/tests/browser/browser_no_browsers_state.js b/browser/components/migration/tests/browser/browser_no_browsers_state.js new file mode 100644 index 0000000000..cd4677f31d --- /dev/null +++ b/browser/components/migration/tests/browser/browser_no_browsers_state.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the wizard switches to the NO_BROWSERS_FOUND page + * when no migrators are detected. + */ +add_task(async function test_browser_no_programs() { + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + sandbox.stub(MigrationUtils, "availableMigratorKeys").get(() => { + return []; + }); + + // Let's enable the Passwords CSV import by default so that it appears + // as a file migrator. + await SpecialPowers.pushPrefEnv({ + set: [["signon.management.page.fileImport.enabled", true]], + }); + + await withMigrationWizardDialog(async prefsWin => { + let dialog = prefsWin.document.querySelector("#migrationWizardDialog"); + let wizard = dialog.querySelector("migration-wizard"); + let shadow = wizard.openOrClosedShadowRoot; + let deck = shadow.querySelector("#wizard-deck"); + + await BrowserTestUtils.waitForMutationCondition( + deck, + { attributeFilter: ["selected-view"] }, + () => { + return ( + deck.getAttribute("selected-view") == + "page-" + MigrationWizardConstants.PAGES.NO_BROWSERS_FOUND + ); + } + ); + + Assert.ok( + true, + "Went to no browser page after attempting to search for migrators." + ); + let chooseImportFromFile = shadow.querySelector("#choose-import-from-file"); + Assert.ok( + !chooseImportFromFile.hidden, + "Selecting a file migrator should still be possible." + ); + }); + + // Now disable all file migrators to make sure that the "Import from file" + // button is hidden. + await SpecialPowers.pushPrefEnv({ + set: [ + ["signon.management.page.fileImport.enabled", false], + ["browser.migrate.bookmarks-file.enabled", false], + ], + }); + + await withMigrationWizardDialog(async prefsWin => { + let dialog = prefsWin.document.querySelector("#migrationWizardDialog"); + let wizard = dialog.querySelector("migration-wizard"); + let shadow = wizard.openOrClosedShadowRoot; + let deck = shadow.querySelector("#wizard-deck"); + + await BrowserTestUtils.waitForMutationCondition( + deck, + { attributeFilter: ["selected-view"] }, + () => { + return ( + deck.getAttribute("selected-view") == + "page-" + MigrationWizardConstants.PAGES.NO_BROWSERS_FOUND + ); + } + ); + + Assert.ok( + true, + "Went to no browser page after attempting to search for migrators." + ); + let chooseImportFromFile = shadow.querySelector("#choose-import-from-file"); + Assert.ok( + chooseImportFromFile.hidden, + "Selecting a file migrator should not be possible." + ); + }); + + sandbox.restore(); +}); diff --git a/browser/components/migration/tests/browser/browser_only_file_migrators.js b/browser/components/migration/tests/browser/browser_only_file_migrators.js new file mode 100644 index 0000000000..ca18a8c0d5 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_only_file_migrators.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the NO_BROWSERS_FOUND page has a button to redirect to the + * selection page when only file migrators are found. + */ +add_task(async function test_only_file_migrators() { + await SpecialPowers.pushPrefEnv({ + set: [["signon.management.page.fileImport.enabled", true]], + }); + + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + sandbox.stub(MigrationUtils, "availableMigratorKeys").get(() => { + return []; + }); + + await withMigrationWizardDialog(async prefsWin => { + let dialog = prefsWin.document.querySelector("#migrationWizardDialog"); + let wizard = dialog.querySelector("migration-wizard"); + let shadow = wizard.openOrClosedShadowRoot; + let deck = shadow.querySelector("#wizard-deck"); + + await BrowserTestUtils.waitForMutationCondition( + deck, + { attributeFilter: ["selected-view"] }, + () => { + return ( + deck.getAttribute("selected-view") == + "page-" + MigrationWizardConstants.PAGES.NO_BROWSERS_FOUND + ); + } + ); + + let chooseImportFileButton = shadow.querySelector( + "#choose-import-from-file" + ); + + let changedToSelectionPage = BrowserTestUtils.waitForMutationCondition( + deck, + { attributeFilter: ["selected-view"] }, + () => { + return ( + deck.getAttribute("selected-view") == + "page-" + MigrationWizardConstants.PAGES.SELECTION + ); + } + ); + chooseImportFileButton.click(); + await changedToSelectionPage; + + // No browser migrators should be listed. + let browserMigratorItems = shadow.querySelectorAll( + `panel-item[type="${MigrationWizardConstants.MIGRATOR_TYPES.BROWSER}"]` + ); + Assert.ok(!browserMigratorItems.length, "No browser migrators listed."); + + // Check to make sure there's at least one file migrator listed. + let fileMigratorItems = shadow.querySelectorAll( + `panel-item[type="${MigrationWizardConstants.MIGRATOR_TYPES.FILE}"]` + ); + + Assert.ok(!!fileMigratorItems.length, "Listed at least one file migrator."); + }); +}); diff --git a/browser/components/migration/tests/browser/browser_permissions.js b/browser/components/migration/tests/browser/browser_permissions.js new file mode 100644 index 0000000000..35d902bb37 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_permissions.js @@ -0,0 +1,166 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.migrate.chrome.get_permissions.enabled", true]], + }); +}); + +/** + * Tests that the migration wizard can request permission from + * the user to read from other browser data directories when + * explicit permission needs to be granted. + * + * This can occur when, for example, Firefox is installed as a + * Snap on Ubuntu Linux. In this state, Firefox does not have + * direct read access to other browser's data directories (although) + * it can tell if they exist. For Chromium-based browsers, this + * means we cannot tell what profiles nor resources are available + * for Chromium-based browsers without read permissions. + * + * Note that the Safari migrator is not tested here, as it has + * its own special permission flow. This is because we can + * determine what resources Safari has before requiring permissions, + * and (as of this writing) Safari does not support multiple + * user profiles. + */ +add_task(async function test_permissions() { + Services.telemetry.clearEvents(); + + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + sandbox + .stub(InternalTestingProfileMigrator.prototype, "canGetPermissions") + .resolves("/some/path"); + + let hasPermissionsStub = sandbox + .stub(InternalTestingProfileMigrator.prototype, "hasPermissions") + .resolves(false); + + let testingMigrator = await MigrationUtils.getMigrator( + InternalTestingProfileMigrator.key + ); + Assert.ok( + testingMigrator, + "Got migrator, even though we don't yet have permission to read its resources." + ); + + sandbox.stub(testingMigrator, "getPermissions").callsFake(async () => { + testingMigrator.flushResourceCache(); + hasPermissionsStub.resolves(true); + return Promise.resolve(true); + }); + + let getResourcesStub = sandbox + .stub(testingMigrator, "getResources") + .resolves([]); + + let migration = waitForTestMigration( + [MigrationUtils.resourceTypes.BOOKMARKS], + [MigrationUtils.resourceTypes.BOOKMARKS], + InternalTestingProfileMigrator.testProfile + ); + + await withMigrationWizardDialog(async prefsWin => { + let dialogBody = prefsWin.document.body; + let wizard = dialogBody.querySelector("migration-wizard"); + let shadow = wizard.openOrClosedShadowRoot; + + // Clear out any pre-existing events events have been logged + Services.telemetry.clearEvents(); + TelemetryTestUtils.assertNumberOfEvents(0); + + let panelItem = shadow.querySelector( + `panel-item[key="${InternalTestingProfileMigrator.key}"]` + ); + panelItem.click(); + + let resourceList = shadow.querySelector(".resource-selection-details"); + Assert.ok( + BrowserTestUtils.isHidden(resourceList), + "Resources list is hidden." + ); + let importButton = shadow.querySelector("#import"); + Assert.ok(BrowserTestUtils.isHidden(importButton), "Import button hidden."); + let noPermissionsMessage = shadow.querySelector(".no-permissions-message"); + Assert.ok( + BrowserTestUtils.isVisible(noPermissionsMessage), + "No permissions message shown." + ); + let getPermissionButton = shadow.querySelector("#get-permissions"); + Assert.ok( + BrowserTestUtils.isVisible(getPermissionButton), + "Get permissions button shown." + ); + + // Now put the permissions functions back into their default + // state - which is the "permission granted" state. + getResourcesStub.restore(); + hasPermissionsStub.restore(); + + let refreshDone = BrowserTestUtils.waitForEvent( + wizard, + "MigrationWizard:Ready" + ); + + getPermissionButton.click(); + + await refreshDone; + Assert.ok(true, "Refreshed migrator list."); + + let wizardDone = BrowserTestUtils.waitForEvent( + wizard, + "MigrationWizard:DoneMigration" + ); + selectResourceTypesAndStartMigration(wizard, [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS, + ]); + await migration; + await wizardDone; + + assertQuantitiesShown(wizard, [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS, + ]); + + let dialog = prefsWin.document.querySelector("#migrationWizardDialog"); + let doneButton = shadow.querySelector( + "div[name='page-progress'] .done-button" + ); + + await new Promise(resolve => prefsWin.requestAnimationFrame(resolve)); + Assert.equal( + shadow.activeElement, + doneButton, + "Done button should be focused." + ); + + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close"); + doneButton.click(); + await dialogClosed; + }); + + TelemetryTestUtils.assertEvents( + [ + { + category: "browser.migration", + method: "linux_perms", + object: "wizard", + value: null, + extra: { + migrator_key: InternalTestingProfileMigrator.key, + }, + }, + ], + { + category: "browser.migration", + method: "linux_perms", + object: "wizard", + } + ); +}); diff --git a/browser/components/migration/tests/browser/browser_safari_passwords.js b/browser/components/migration/tests/browser/browser_safari_passwords.js new file mode 100644 index 0000000000..c005342b46 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_safari_passwords.js @@ -0,0 +1,468 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SafariProfileMigrator } = ChromeUtils.importESModule( + "resource:///modules/SafariProfileMigrator.sys.mjs" +); +const { LoginCSVImport } = ChromeUtils.importESModule( + "resource://gre/modules/LoginCSVImport.sys.mjs" +); + +const TEST_FILE_PATH = getTestFilePath("dummy_file.csv"); + +// We use MockFilePicker to simulate a native file picker, and prepare it +// to return a dummy file pointed at TEST_FILE_PATH. The file at +// TEST_FILE_PATH is not required (nor expected) to exist. +const { MockFilePicker } = SpecialPowers; + +add_setup(async function () { + MockFilePicker.init(window); + registerCleanupFunction(() => { + MockFilePicker.cleanup(); + }); + await SpecialPowers.pushPrefEnv({ + set: [["signon.management.page.fileImport.enabled", true]], + }); +}); + +/** + * A helper function that does most of the heavy lifting for the tests in + * this file. Specfically, it takes care of: + * + * 1. Stubbing out the various hunks of the SafariProfileMigrator in order + * to simulate a migration without actually performing one, since the + * migrator itself isn't being tested here. + * 2. Stubbing out parts of MigrationUtils and LoginCSVImport to have a + * consistent reporting on how many things are imported. + * 3. Setting up the MockFilePicker if expectsFilePicker is true to return + * the TEST_FILE_PATH. + * 4. Opens up the migration wizard, and chooses to import both BOOKMARKS + * and PASSWORDS, and then clicks "Import". + * 5. Waits for the migration wizard to show the Safari password import + * instructions. + * 6. Runs taskFn + * 7. Closes the migration dialog. + * + * @param {boolean} expectsFilePicker + * True if the MockFilePicker should be set up to return TEST_FILE_PATH. + * @param {boolean} migrateBookmarks + * True if bookmarks should be migrated alongside passwords. If not, only + * passwords will be migrated. + * @param {boolean} shouldPasswordImportFail + * True if importing from the CSV file should fail. + * @param {Function} taskFn + * An asynchronous function that takes the following parameters in this + * order: + * + * {Element} wizard + * The opened migration wizard + * {Promise} filePickerShownPromise + * A Promise that resolves once the MockFilePicker has closed. This is + * undefined if expectsFilePicker was false. + * {object} importFromCSVStub + * The Sinon stub object for LoginCSVImport.importFromCSV. This can be + * used to check to see whether it was called. + * {Promise} didMigration + * A Promise that resolves to true once the migration completes. + * {Promise} wizardDone + * A Promise that resolves once the migration wizard reports that a + * migration has completed. + * @returns {Promise<undefined>} + */ +async function testSafariPasswordHelper( + expectsFilePicker, + migrateBookmarks, + shouldPasswordImportFail, + taskFn +) { + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + let safariMigrator = new SafariProfileMigrator(); + sandbox.stub(MigrationUtils, "getMigrator").resolves(safariMigrator); + + // We're not testing the permission flow here, so let's pretend that we + // always have permission to read resources from the disk. + sandbox + .stub(SafariProfileMigrator.prototype, "hasPermissions") + .resolves(true); + + // Have the migrator claim that only BOOKMARKS are only available. + sandbox + .stub(SafariProfileMigrator.prototype, "getMigrateData") + .resolves(MigrationUtils.resourceTypes.BOOKMARKS); + + let migrateStub; + let didMigration = new Promise(resolve => { + migrateStub = sandbox + .stub(SafariProfileMigrator.prototype, "migrate") + .callsFake((aResourceTypes, aStartup, aProfile, aProgressCallback) => { + if (!migrateBookmarks) { + Assert.ok( + false, + "Should not have called migrate when only migrating Safari passwords." + ); + } + + Assert.ok( + !aStartup, + "Migrator should not have been called as a startup migration." + ); + Assert.ok( + aResourceTypes & MigrationUtils.resourceTypes.BOOKMARKS, + "Should have requested to migrate the BOOKMARKS resource." + ); + Assert.ok( + !(aResourceTypes & MigrationUtils.resourceTypes.PASSWORDS), + "Should not have requested to migrate the PASSWORDS resource." + ); + + aProgressCallback(MigrationUtils.resourceTypes.BOOKMARKS, true); + Services.obs.notifyObservers(null, "Migration:Ended"); + resolve(); + }); + }); + + // We'll pretend we added EXPECTED_QUANTITY passwords from the Safari + // password file. + let results = []; + for (let i = 0; i < EXPECTED_QUANTITY; ++i) { + results.push({ result: "added" }); + } + let importFromCSVStub = sandbox.stub(LoginCSVImport, "importFromCSV"); + + if (shouldPasswordImportFail) { + importFromCSVStub.rejects(new Error("Some error message")); + } else { + importFromCSVStub.resolves(results); + } + + sandbox.stub(MigrationUtils, "_importQuantities").value({ + bookmarks: EXPECTED_QUANTITY, + }); + + let filePickerShownPromise; + + if (expectsFilePicker) { + MockFilePicker.reset(); + + let dummyFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + dummyFile.initWithPath(TEST_FILE_PATH); + filePickerShownPromise = new Promise(resolve => { + MockFilePicker.showCallback = () => { + Assert.ok(true, "Filepicker shown."); + MockFilePicker.setFiles([dummyFile]); + resolve(); + }; + }); + MockFilePicker.returnValue = MockFilePicker.returnOK; + } + + await withMigrationWizardDialog(async prefsWin => { + let dialogBody = prefsWin.document.body; + let wizard = dialogBody.querySelector("migration-wizard"); + let wizardDone = BrowserTestUtils.waitForEvent( + wizard, + "MigrationWizard:DoneMigration" + ); + + let shadow = wizard.openOrClosedShadowRoot; + + info("Choosing Safari"); + let panelItem = shadow.querySelector( + `panel-item[key="${SafariProfileMigrator.key}"]` + ); + panelItem.click(); + + let resourceTypeList = shadow.querySelector("#resource-type-list"); + + // Let's choose whether to import BOOKMARKS first. + let bookmarksNode = resourceTypeList.querySelector( + `label[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS}"]` + ); + bookmarksNode.control.checked = migrateBookmarks; + + // Let's make sure that PASSWORDS is displayed despite the migrator only + // (currently) returning BOOKMARKS as an available resource to migrate. + let passwordsNode = resourceTypeList.querySelector( + `label[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS}"]` + ); + Assert.ok( + !passwordsNode.hidden, + "PASSWORDS should be available to import from." + ); + passwordsNode.control.checked = true; + + let deck = shadow.querySelector("#wizard-deck"); + let switchedToSafariPermissionPage = + BrowserTestUtils.waitForMutationCondition( + deck, + { attributeFilter: ["selected-view"] }, + () => { + return ( + deck.getAttribute("selected-view") == + "page-" + MigrationWizardConstants.PAGES.SAFARI_PASSWORD_PERMISSION + ); + } + ); + + let importButton = shadow.querySelector("#import"); + importButton.click(); + await switchedToSafariPermissionPage; + Assert.ok(true, "Went to Safari permission page after attempting import."); + + await taskFn( + wizard, + filePickerShownPromise, + importFromCSVStub, + didMigration, + migrateStub, + wizardDone + ); + + let dialog = prefsWin.document.querySelector("#migrationWizardDialog"); + let doneButton = shadow.querySelector( + "div[name='page-progress'] .done-button" + ); + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close"); + + doneButton.click(); + await dialogClosed; + }); + + sandbox.restore(); + MockFilePicker.reset(); +} + +/** + * Tests the flow of importing passwords from Safari via an + * exported CSV file. + */ +add_task(async function test_safari_password_do_import() { + await testSafariPasswordHelper( + true, + true, + false, + async ( + wizard, + filePickerShownPromise, + importFromCSVStub, + didMigration, + migrateStub, + wizardDone + ) => { + let shadow = wizard.openOrClosedShadowRoot; + let safariPasswordImportSelect = shadow.querySelector( + "#safari-password-import-select" + ); + safariPasswordImportSelect.click(); + await filePickerShownPromise; + Assert.ok(true, "File picker was shown."); + + await didMigration; + Assert.ok(importFromCSVStub.called, "Importing from CSV was called."); + + await wizardDone; + + assertQuantitiesShown(wizard, [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS, + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS, + ]); + } + ); +}); + +/** + * Tests that only passwords get imported if the user only opts + * to import passwords, and that nothing else gets imported. + */ +add_task(async function test_safari_password_only_do_import() { + await testSafariPasswordHelper( + true, + false, + false, + async ( + wizard, + filePickerShownPromise, + importFromCSVStub, + didMigration, + migrateStub, + wizardDone + ) => { + let shadow = wizard.openOrClosedShadowRoot; + let safariPasswordImportSelect = shadow.querySelector( + "#safari-password-import-select" + ); + safariPasswordImportSelect.click(); + await filePickerShownPromise; + Assert.ok(true, "File picker was shown."); + + await wizardDone; + + assertQuantitiesShown(wizard, [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS, + ]); + + Assert.ok(importFromCSVStub.called, "Importing from CSV was called."); + Assert.ok( + !migrateStub.called, + "SafariProfileMigrator.migrate was not called." + ); + } + ); +}); + +/** + * Tests the flow of importing passwords from Safari when the file + * import fails. + */ +add_task(async function test_safari_password_empty_csv_file() { + await testSafariPasswordHelper( + true, + true, + true, + async ( + wizard, + filePickerShownPromise, + importFromCSVStub, + didMigration, + migrateStub, + wizardDone + ) => { + let shadow = wizard.openOrClosedShadowRoot; + let safariPasswordImportSelect = shadow.querySelector( + "#safari-password-import-select" + ); + safariPasswordImportSelect.click(); + await filePickerShownPromise; + Assert.ok(true, "File picker was shown."); + + await didMigration; + Assert.ok(importFromCSVStub.called, "Importing from CSV was called."); + + await wizardDone; + + let headerL10nID = + shadow.querySelector("#progress-header").dataset.l10nId; + Assert.equal( + headerL10nID, + "migration-wizard-progress-done-with-warnings-header" + ); + + let progressGroup = shadow.querySelector( + `.resource-progress-group[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS}"` + ); + let progressIcon = progressGroup.querySelector(".progress-icon"); + let messageText = + progressGroup.querySelector(".message-text").textContent; + + Assert.equal( + progressIcon.getAttribute("state"), + "warning", + "Icon should be in the warning state." + ); + Assert.stringMatches( + messageText, + /file doesn’t include any valid password data/ + ); + } + ); +}); + +/** + * Tests that the user can skip importing passwords from Safari. + */ +add_task(async function test_safari_password_skip() { + await testSafariPasswordHelper( + false, + true, + false, + async ( + wizard, + filePickerShownPromise, + importFromCSVStub, + didMigration, + migrateStub, + wizardDone + ) => { + let shadow = wizard.openOrClosedShadowRoot; + let safariPasswordImportSkip = shadow.querySelector( + "#safari-password-import-skip" + ); + safariPasswordImportSkip.click(); + + await didMigration; + Assert.ok(!MockFilePicker.shown, "Never showed the file picker."); + Assert.ok( + !importFromCSVStub.called, + "Importing from CSV was never called." + ); + + await wizardDone; + + assertQuantitiesShown(wizard, [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS, + ]); + } + ); +}); + +/** + * Tests that importing from passwords for Safari doesn't exist if + * signon.management.page.fileImport.enabled is false. + */ +add_task(async function test_safari_password_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [["signon.management.page.fileImport.enabled", false]], + }); + + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + let safariMigrator = new SafariProfileMigrator(); + sandbox.stub(MigrationUtils, "getMigrator").resolves(safariMigrator); + + // We're not testing the permission flow here, so let's pretend that we + // always have permission to read resources from the disk. + sandbox + .stub(SafariProfileMigrator.prototype, "hasPermissions") + .resolves(true); + + // Have the migrator claim that only BOOKMARKS are only available. + sandbox + .stub(SafariProfileMigrator.prototype, "getMigrateData") + .resolves(MigrationUtils.resourceTypes.BOOKMARKS); + + await withMigrationWizardDialog(async prefsWin => { + let dialogBody = prefsWin.document.body; + let wizard = dialogBody.querySelector("migration-wizard"); + + let shadow = wizard.openOrClosedShadowRoot; + + info("Choosing Safari"); + let panelItem = shadow.querySelector( + `panel-item[key="${SafariProfileMigrator.key}"]` + ); + panelItem.click(); + + let resourceTypeList = shadow.querySelector("#resource-type-list"); + + // Let's make sure that PASSWORDS is displayed despite the migrator only + // (currently) returning BOOKMARKS as an available resource to migrate. + let passwordsNode = resourceTypeList.querySelector( + `label[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS}"]` + ); + Assert.ok( + passwordsNode.hidden, + "PASSWORDS should not be available to import from." + ); + }); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/migration/tests/browser/browser_safari_permissions.js b/browser/components/migration/tests/browser/browser_safari_permissions.js new file mode 100644 index 0000000000..bac56866f0 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_safari_permissions.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SafariProfileMigrator } = ChromeUtils.importESModule( + "resource:///modules/SafariProfileMigrator.sys.mjs" +); + +/** + * Tests that if we don't have permission to read the contents + * of ~/Library/Safari, that we can ask for permission to do that. + * + * This involves presenting the user with some instructions, and then + * showing a native folder picker for the user to select the + * ~/Library/Safari folder. This seems to give us read access to the + * folder contents. + * + * Revoking permissions for reading the ~/Library/Safari folder is + * not something that we know how to do just yet. It seems to be + * something involving macOS's System Integrity Protection. This test + * mocks out and simulates the actual permissions mechanism to make + * this test run reliably and repeatably. + */ +add_task(async function test_safari_permissions() { + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + let hasPermissionsStub = sandbox + .stub(SafariProfileMigrator.prototype, "hasPermissions") + .resolves(false); + + let safariMigrator = await MigrationUtils.getMigrator( + SafariProfileMigrator.key + ); + Assert.ok( + safariMigrator, + "Got migrator, even though we don't yet have permission to read its resources." + ); + + sandbox.stub(MigrationUtils, "getMigrator").resolves(safariMigrator); + + sandbox.stub(safariMigrator, "getPermissions").callsFake(async () => { + hasPermissionsStub.resolves(true); + return Promise.resolve(true); + }); + + sandbox.stub(safariMigrator, "getResources").callsFake(() => { + return Promise.resolve([ + { + type: MigrationUtils.resourceTypes.BOOKMARKS, + migrate: () => {}, + }, + ]); + }); + + let didMigration = new Promise(resolve => { + sandbox + .stub(safariMigrator, "migrate") + .callsFake((aResourceTypes, aStartup, aProfile, aProgressCallback) => { + Assert.ok( + !aStartup, + "Migrator should not have been called as a startup migration." + ); + + aProgressCallback(MigrationUtils.resourceTypes.BOOKMARKS); + Services.obs.notifyObservers(null, "Migration:Ended"); + resolve(); + }); + }); + + await withMigrationWizardDialog(async prefsWin => { + let dialogBody = prefsWin.document.body; + let wizard = dialogBody.querySelector("migration-wizard"); + let wizardDone = BrowserTestUtils.waitForEvent( + wizard, + "MigrationWizard:DoneMigration" + ); + + let shadow = wizard.openOrClosedShadowRoot; + + info("Choosing Safari"); + let panelItem = shadow.querySelector( + `panel-item[key="${SafariProfileMigrator.key}"]` + ); + panelItem.click(); + + // Let's just choose "Bookmarks" for now. + let resourceTypeList = shadow.querySelector("#resource-type-list"); + let resourceNodes = resourceTypeList.querySelectorAll( + `label[data-resource-type]` + ); + for (let resourceNode of resourceNodes) { + resourceNode.control.checked = + resourceNode.dataset.resourceType == + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS; + } + + let deck = shadow.querySelector("#wizard-deck"); + let switchedToSafariPermissionPage = + BrowserTestUtils.waitForMutationCondition( + deck, + { attributeFilter: ["selected-view"] }, + () => { + return ( + deck.getAttribute("selected-view") == + "page-" + MigrationWizardConstants.PAGES.SAFARI_PERMISSION + ); + } + ); + + let importButton = shadow.querySelector("#import"); + importButton.click(); + await switchedToSafariPermissionPage; + Assert.ok(true, "Went to Safari permission page after attempting import."); + + let requestPermissions = shadow.querySelector( + "#safari-request-permissions" + ); + requestPermissions.click(); + await didMigration; + Assert.ok(true, "Completed migration"); + + let dialog = prefsWin.document.querySelector("#migrationWizardDialog"); + let doneButton = shadow.querySelector( + "div[name='page-progress'] .done-button" + ); + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close"); + + doneButton.click(); + await dialogClosed; + await wizardDone; + }); +}); diff --git a/browser/components/migration/tests/browser/dummy_file.csv b/browser/components/migration/tests/browser/dummy_file.csv new file mode 100644 index 0000000000..48a099ab76 --- /dev/null +++ b/browser/components/migration/tests/browser/dummy_file.csv @@ -0,0 +1 @@ +This file intentionally left blank.
\ No newline at end of file diff --git a/browser/components/migration/tests/browser/head.js b/browser/components/migration/tests/browser/head.js new file mode 100644 index 0000000000..d3d188a7e1 --- /dev/null +++ b/browser/components/migration/tests/browser/head.js @@ -0,0 +1,534 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../head-common.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/migration/tests/browser/head-common.js", + this +); + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); +const { InternalTestingProfileMigrator } = ChromeUtils.importESModule( + "resource:///modules/InternalTestingProfileMigrator.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const DIALOG_URL = + "chrome://browser/content/migration/migration-dialog-window.html"; + +/** + * We'll have this be our magic number of quantities of various imports. + * We will use Sinon to prepare MigrationUtils to presume that this was + * how many of each quantity-supported resource type was imported. + */ +const EXPECTED_QUANTITY = 123; + +/** + * These are the resource types that currently display their import success + * message with a quantity. + */ +const RESOURCE_TYPES_WITH_QUANTITIES = [ + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS, + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY, + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS, + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA, + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PAYMENT_METHODS, + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS, +]; + +/** + * The withMigrationWizardDialog callback, called after the + * dialog has loaded and the wizard is ready. + * + * @callback withMigrationWizardDialogCallback + * @param {DOMWindow} window + * The content window of the migration wizard subdialog frame. + * @returns {Promise<undefined>} + */ + +/** + * Opens the migration wizard HTML5 dialog in about:preferences in the + * current window's selected tab, runs an async taskFn, and then + * cleans up by loading about:blank in the tab before resolving. + * + * @param {withMigrationWizardDialogCallback} taskFn + * An async test function to be called while the migration wizard + * dialog is open. + * @returns {Promise<undefined>} + */ +async function withMigrationWizardDialog(taskFn) { + let migrationDialogPromise = waitForMigrationWizardDialogTab(); + await MigrationUtils.showMigrationWizard(window, {}); + let prefsBrowser = await migrationDialogPromise; + + try { + await taskFn(prefsBrowser.contentWindow); + } finally { + if (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.getTabForBrowser(prefsBrowser)); + } else { + BrowserTestUtils.startLoadingURIString(prefsBrowser, "about:blank"); + await BrowserTestUtils.browserLoaded(prefsBrowser); + } + } +} + +/** + * Returns a Promise that resolves when an about:preferences tab opens + * in the current window which loads the migration wizard dialog. + * The Promise will wait until the migration wizard reports that it + * is ready with the "MigrationWizard:Ready" event. + * + * @returns {Promise<browser>} + * Resolves with the about:preferences browser element. + */ +async function waitForMigrationWizardDialogTab() { + let wizardReady = BrowserTestUtils.waitForEvent( + window, + "MigrationWizard:Ready" + ); + + let tab; + if (gBrowser.selectedTab.isEmpty) { + tab = gBrowser.selectedTab; + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url => { + return url.startsWith("about:preferences"); + }); + } else { + tab = await BrowserTestUtils.waitForNewTab(gBrowser, url => { + return url.startsWith("about:preferences"); + }); + } + + await wizardReady; + info("Done waiting - migration subdialog loaded and ready."); + + return tab.linkedBrowser; +} + +/** + * A helper function that prepares the InternalTestingProfileMigrator + * with some set of fake available resources, and resolves a Promise + * when the InternalTestingProfileMigrator is used for a migration. + * + * @param {number[]} availableResourceTypes + * An array of resource types from MigrationUtils.resourcesTypes. + * A single MigrationResource will be created per type, with a + * no-op migrate function. + * @param {number[]} expectedResourceTypes + * An array of resource types from MigrationUtils.resourceTypes. + * These are the resource types that are expected to be passed + * to the InternalTestingProfileMigrator.migrate function. + * @param {object|string} expectedProfile + * The profile object or string that is expected to be passed + * to the InternalTestingProfileMigrator.migrate function. + * @param {number[]} [errorResourceTypes=[]] + * Resource types that we should pretend have failed to complete + * their migration properly. + * @param {number} [totalExtensions=1] + * If migrating extensions, the total that should be reported to + * have been found from the source browser. + * @param {number} [matchedExtensions=1] + * If migrating extensions, the number of extensions that should + * be reported as having equivalent matches for this browser. + * @returns {Promise<undefined>} + */ +async function waitForTestMigration( + availableResourceTypes, + expectedResourceTypes, + expectedProfile, + errorResourceTypes = [], + totalExtensions = 1, + matchedExtensions = 1 +) { + let sandbox = sinon.createSandbox(); + let sourceHistogram = TelemetryTestUtils.getAndClearHistogram( + "FX_MIGRATION_SOURCE_BROWSER" + ); + let usageHistogram = + TelemetryTestUtils.getAndClearKeyedHistogram("FX_MIGRATION_USAGE"); + let errorHistogram = TelemetryTestUtils.getAndClearKeyedHistogram( + "FX_MIGRATION_ERRORS" + ); + + // Fake out the getResources method of the migrator so that we return + // a single fake MigratorResource per availableResourceType. + sandbox + .stub(InternalTestingProfileMigrator.prototype, "getResources") + .callsFake(aProfile => { + Assert.deepEqual( + aProfile, + expectedProfile, + "Should have gotten the expected profile." + ); + return Promise.resolve( + availableResourceTypes.map(resourceType => { + return { + type: resourceType, + migrate: () => {}, + }; + }) + ); + }); + + sandbox.stub(MigrationUtils, "_importQuantities").value({ + bookmarks: EXPECTED_QUANTITY, + history: EXPECTED_QUANTITY, + logins: EXPECTED_QUANTITY, + cards: EXPECTED_QUANTITY, + }); + + sandbox + .stub(MigrationUtils, "getSourceIdForTelemetry") + .withArgs(InternalTestingProfileMigrator.key) + .returns(InternalTestingProfileMigrator.sourceID); + + // Fake out the migrate method of the migrator and assert that the + // next time it's called, its arguments match our expectations. + return new Promise(resolve => { + sandbox + .stub(InternalTestingProfileMigrator.prototype, "migrate") + .callsFake((aResourceTypes, aStartup, aProfile, aProgressCallback) => { + Assert.ok( + !aStartup, + "Migrator should not have been called as a startup migration." + ); + + let bitMask = 0; + for (let resourceType of expectedResourceTypes) { + bitMask |= resourceType; + } + + Assert.deepEqual( + aResourceTypes, + bitMask, + "Got the expected resource types" + ); + Assert.deepEqual( + aProfile, + expectedProfile, + "Got the expected profile object" + ); + + for (let resourceType of expectedResourceTypes) { + let shouldError = errorResourceTypes.includes(resourceType); + if ( + resourceType == MigrationUtils.resourceTypes.EXTENSIONS && + !shouldError + ) { + let progressValue; + if (totalExtensions == matchedExtensions) { + progressValue = MigrationWizardConstants.PROGRESS_VALUE.SUCCESS; + } else if ( + totalExtensions > matchedExtensions && + matchedExtensions + ) { + progressValue = MigrationWizardConstants.PROGRESS_VALUE.INFO; + } else { + Assert.ok( + false, + "Total and matched extensions should be greater than 0 on success." + + `Total: ${totalExtensions}, Matched: ${matchedExtensions}` + ); + } + aProgressCallback(resourceType, !shouldError, { + totalExtensions: Array(totalExtensions), + importedExtensions: Array(matchedExtensions), + progressValue, + }); + } else { + aProgressCallback(resourceType, !shouldError); + } + } + + let usageHistogramSnapshot = + usageHistogram.snapshot()[InternalTestingProfileMigrator.key]; + + let errorHistogramSnapshot = + errorHistogram.snapshot()[InternalTestingProfileMigrator.key]; + + for (let resourceTypeName in MigrationUtils.resourceTypes) { + let resourceType = MigrationUtils.resourceTypes[resourceTypeName]; + if (resourceType == MigrationUtils.resourceTypes.ALL) { + continue; + } + + if (expectedResourceTypes.includes(resourceType)) { + Assert.equal( + usageHistogramSnapshot.values[Math.log2(resourceType)], + 1, + `Should have set resource type ${resourceTypeName} on the FX_MIGRATION_USAGE keyed histogram.` + ); + + if (errorResourceTypes.includes(resourceType)) { + Assert.equal( + errorHistogramSnapshot.values[Math.log2(resourceType)], + 1, + `Should have set resource type ${resourceTypeName} on the FX_MIGRATION_ERRORS keyed histogram.` + ); + } + } else { + let value = usageHistogramSnapshot.values[Math.log2(resourceType)]; + Assert.ok( + value === 0 || value === undefined, + `Should not have set resource type ${resourceTypeName} on the FX_MIGRATION_USAGE keyed histogram.` + ); + } + } + + Services.obs.notifyObservers(null, "Migration:Ended"); + + TelemetryTestUtils.assertHistogram( + sourceHistogram, + InternalTestingProfileMigrator.sourceID, + 1 + ); + + resolve(); + }); + }).finally(async () => { + sandbox.restore(); + + // MigratorBase caches resources fetched by the getResources method + // as a performance optimization. In order to allow different tests + // to have different available resources, we call into a special + // method of InternalTestingProfileMigrator that clears that + // cache. + let migrator = await MigrationUtils.getMigrator( + InternalTestingProfileMigrator.key + ); + migrator.flushResourceCache(); + }); +} + +/** + * Takes a MigrationWizard element and chooses the + * InternalTestingProfileMigrator as the browser to migrate from. Then, it + * checks the checkboxes associated with the selectedResourceTypes and + * unchecks the rest before clicking the "Import" button. + * + * @param {Element} wizard + * The MigrationWizard element. + * @param {string[]} selectedResourceTypes + * An array of resource type strings from + * MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES. + * @param {string} [migratorKey=InternalTestingProfileMigrator.key] + * The key for the migrator to use. Defaults to the + * InternalTestingProfileMigrator. + */ +async function selectResourceTypesAndStartMigration( + wizard, + selectedResourceTypes, + migratorKey = InternalTestingProfileMigrator.key +) { + let shadow = wizard.openOrClosedShadowRoot; + + // First, select the InternalTestingProfileMigrator browser. + let selector = shadow.querySelector("#browser-profile-selector"); + selector.click(); + + await new Promise(resolve => { + shadow + .querySelector("panel-list") + .addEventListener("shown", resolve, { once: true }); + }); + + let panelItem = shadow.querySelector(`panel-item[key="${migratorKey}"]`); + panelItem.click(); + + // And then check the right checkboxes for the resource types. + let resourceTypeList = shadow.querySelector("#resource-type-list"); + for (let resourceType of getChoosableResourceTypes()) { + let node = resourceTypeList.querySelector( + `label[data-resource-type="${resourceType}"]` + ); + node.control.checked = selectedResourceTypes.includes(resourceType); + } + + let importButton = shadow.querySelector("#import"); + importButton.click(); +} + +/** + * Assert that the resource types passed in expectedResourceTypes are + * showing a success state after a migration, and if they are part of + * the RESOURCE_TYPES_WITH_QUANTITIES group, that they're showing the + * EXPECTED_QUANTITY magic number in their success message. Otherwise, + * we (currently) check that they show the empty string. + * + * @param {Element} wizard + * The MigrationWizard element. + * @param {string[]} expectedResourceTypes + * An array of resource type strings from + * MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES. + * @param {string[]} [warningResourceTypes=[]] + * An array of resource type strings from + * MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES. These + * are the resources that should be showing a warning message. + */ +function assertQuantitiesShown( + wizard, + expectedResourceTypes, + warningResourceTypes = [] +) { + let shadow = wizard.openOrClosedShadowRoot; + + // Make sure that we're showing the progress page first. + let deck = shadow.querySelector("#wizard-deck"); + Assert.equal( + deck.selectedViewName, + `page-${MigrationWizardConstants.PAGES.PROGRESS}` + ); + + let headerL10nID = shadow.querySelector("#progress-header").dataset.l10nId; + if (warningResourceTypes.length) { + Assert.equal( + headerL10nID, + "migration-wizard-progress-done-with-warnings-header" + ); + } else { + Assert.equal(headerL10nID, "migration-wizard-progress-done-header"); + } + + // Go through each displayed resource and make sure that only the + // ones that are expected are shown, and are showing the right + // success message. + + let progressGroups = shadow.querySelectorAll(".resource-progress-group"); + for (let progressGroup of progressGroups) { + if (expectedResourceTypes.includes(progressGroup.dataset.resourceType)) { + let progressIcon = progressGroup.querySelector(".progress-icon"); + let messageText = + progressGroup.querySelector(".message-text").textContent; + + if (warningResourceTypes.includes(progressGroup.dataset.resourceType)) { + Assert.equal( + progressIcon.getAttribute("state"), + "warning", + "Should be showing the warning icon state." + ); + } else { + Assert.equal( + progressIcon.getAttribute("state"), + "success", + "Should be showing the success icon state." + ); + } + + if ( + RESOURCE_TYPES_WITH_QUANTITIES.includes( + progressGroup.dataset.resourceType + ) + ) { + if ( + progressGroup.dataset.resourceType == + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY + ) { + // HISTORY is a special case that doesn't show the number of imported + // history entries, but instead shows the maximum number of days of history + // that might have been imported. + Assert.notEqual( + messageText.indexOf(MigrationUtils.HISTORY_MAX_AGE_IN_DAYS), + -1, + `Found expected maximum number of days of history: ${messageText}` + ); + } else if ( + progressGroup.dataset.resourceType == + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA + ) { + // FORMDATA is another special case, because we simply show "Form history" as + // the message string, rather than a particular quantity. + Assert.equal( + messageText, + "Form history", + `Found expected form data string: ${messageText}` + ); + } else if ( + progressGroup.dataset.resourceType == + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS + ) { + // waitForTestMigration by default sets up a "successful" migration of 1 + // extension. + Assert.stringMatches(messageText, "1 extension"); + } else { + Assert.notEqual( + messageText.indexOf(EXPECTED_QUANTITY), + -1, + `Found expected quantity in message string: ${messageText}` + ); + } + } else { + // If you've found yourself here, and this is failing, it's probably because you've + // updated MigrationWizardParent.#getStringForImportQuantity to return a string for + // a resource type that's not in RESOURCE_TYPES_WITH_QUANTITIES, and you'll need + // to modify this function to check for that string. + Assert.equal( + messageText, + "", + "Expected the empty string if the resource type " + + "isn't in RESOURCE_TYPES_WITH_QUANTITIES" + ); + } + } else { + Assert.ok( + BrowserTestUtils.isHidden(progressGroup), + `Resource progress group for ${progressGroup.dataset.resourceType}` + + ` should be hidden.` + ); + } + } +} + +/** + * Translates an entrypoint string into the proper numeric value for the + * FX_MIGRATION_ENTRY_POINT_CATEGORICAL histogram. + * + * @param {string} entrypoint + * The entrypoint to translate from MIGRATION_ENTRYPOINTS. + * @returns {number} + * The numeric index value for the FX_MIGRATION_ENTRY_POINT_CATEGORICAL + * histogram. + */ +function getEntrypointHistogramIndex(entrypoint) { + switch (entrypoint) { + case MigrationUtils.MIGRATION_ENTRYPOINTS.FIRSTRUN: { + return 1; + } + case MigrationUtils.MIGRATION_ENTRYPOINTS.FXREFRESH: { + return 2; + } + case MigrationUtils.MIGRATION_ENTRYPOINTS.PLACES: { + return 3; + } + case MigrationUtils.MIGRATION_ENTRYPOINTS.PASSWORDS: { + return 4; + } + case MigrationUtils.MIGRATION_ENTRYPOINTS.NEWTAB: { + return 5; + } + case MigrationUtils.MIGRATION_ENTRYPOINTS.FILE_MENU: { + return 6; + } + case MigrationUtils.MIGRATION_ENTRYPOINTS.HELP_MENU: { + return 7; + } + case MigrationUtils.MIGRATION_ENTRYPOINTS.BOOKMARKS_TOOLBAR: { + return 8; + } + case MigrationUtils.MIGRATION_ENTRYPOINTS.PREFERENCES: { + return 9; + } + case MigrationUtils.MIGRATION_ENTRYPOINTS.UNKNOWN: + // Intentional fall-through + default: { + return 0; // Unknown + } + } +} diff --git a/browser/components/migration/tests/chrome/chrome.toml b/browser/components/migration/tests/chrome/chrome.toml new file mode 100644 index 0000000000..8f1c943f31 --- /dev/null +++ b/browser/components/migration/tests/chrome/chrome.toml @@ -0,0 +1,5 @@ +[DEFAULT] +skip-if = ["os == 'android'"] +support-files = ["../head-common.js"] + +["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..d991cce114 --- /dev/null +++ b/browser/components/migration/tests/chrome/test_migration_wizard.html @@ -0,0 +1,1533 @@ +<!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://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script src="head-common.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> + /* import-globals-from ../head-common.js */ + + "use strict"; + + const { BrowserTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ); + + const MIGRATOR_PROFILE_INSTANCES = [ + { + key: "some-browser-0", + type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER, + displayName: "Some Browser 0", + resourceTypes: ["HISTORY", "FORMDATA", "PASSWORDS", "BOOKMARKS", "EXTENSIONS"], + profile: { id: "person-2", name: "Person 2" }, + hasPermissions: true, + }, + { + key: "some-browser-1", + type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER, + displayName: "Some Browser 1", + resourceTypes: ["HISTORY", "BOOKMARKS"], + profile: null, + hasPermissions: true, + }, + ]; + + let gWiz = null; + let gShadowRoot = null; + let gDeck = null; + + /** + * Returns the .resource-progress-group div for a particular resource + * type. + * + * @param {string} displayedResourceType + * One of the constants belonging to + * MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES. + * @returns {Element} + */ + function getResourceGroup(displayedResourceType) { + return gShadowRoot.querySelector( + `.resource-progress-group[data-resource-type="${displayedResourceType}"]` + ); + } + + add_setup(async function() { + gWiz = document.getElementById("test-wizard"); + gShadowRoot = gWiz.openOrClosedShadowRoot; + gDeck = gShadowRoot.querySelector("#wizard-deck"); + }); + + /** + * Tests that the MigrationWizard:RequestState event is fired when the + * <migration-wizard> is added to the DOM if the auto-request-state attribute + * is set, and then ensures that the starting page is correct. + * + * This also tests that the MigrationWizard:RequestState is not fired automatically + * if the auto-request-state attribute is not set, but is then fired upon calling + * requestState(). + * + * This uses a dynamically created <migration-wizard> instead of the one already + * in the content div to make sure that the init event is captured. + */ + add_task(async function test_init_event() { + const REQUEST_STATE_EVENT = "MigrationWizard:RequestState"; + + let wiz = document.createElement("migration-wizard"); + wiz.toggleAttribute("auto-request-state", true); + let content = document.getElementById("content"); + let promise = new Promise(resolve => { + content.addEventListener(REQUEST_STATE_EVENT, resolve, { + once: true, + }); + }); + content.appendChild(wiz); + await promise; + ok(true, `Saw ${REQUEST_STATE_EVENT} event.`); + let shadowRoot = wiz.openOrClosedShadowRoot; + let deck = shadowRoot.querySelector("#wizard-deck"); + is( + deck.selectedViewName, + "page-loading", + "Should have the loading page selected" + ); + wiz.remove(); + + wiz.toggleAttribute("auto-request-state", false); + let sawEvent = false; + let handler = () => { + sawEvent = true; + }; + content.addEventListener(REQUEST_STATE_EVENT, handler); + content.appendChild(wiz); + ok(!sawEvent, `Should not have seen ${REQUEST_STATE_EVENT} event.`); + content.removeEventListener(REQUEST_STATE_EVENT, handler); + + promise = new Promise(resolve => { + content.addEventListener(REQUEST_STATE_EVENT, resolve, { + once: true, + }); + }); + wiz.requestState(); + await promise; + ok(true, `Saw ${REQUEST_STATE_EVENT} event.`); + wiz.remove(); + }); + + /** + * Tests that the wizard can show a list of browser and profiles. + */ + add_task(async function test_selection() { + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: MIGRATOR_PROFILE_INSTANCES, + showImportAll: false, + }); + + let selector = gShadowRoot.querySelector("#browser-profile-selector"); + let preamble = gShadowRoot.querySelector(".resource-selection-preamble"); + ok(!isHidden(preamble), "preamble should shown."); + + let panelList = gShadowRoot.querySelector("panel-list"); + is(panelList.childElementCount, 2, "Should have two child elements"); + + let resourceTypeList = gShadowRoot.querySelector("#resource-type-list"); + let details = gShadowRoot.querySelector("details"); + ok(details.open, "Details should be open"); + + // Test that the resource type checkboxes are shown or hidden depending on + // which resourceTypes are included with the MigratorProfileInstance. + for (let migratorInstance of MIGRATOR_PROFILE_INSTANCES) { + selector.click(); + await new Promise(resolve => { + gShadowRoot + .querySelector("panel-list") + .addEventListener("shown", resolve, { once: true }); + }); + let panelItem = gShadowRoot.querySelector( + `panel-item[key="${migratorInstance.key}"]` + ); + ok(panelItem, "Should find panel-item."); + panelItem.click(); + + is( + selector.querySelector("#migrator-name").textContent, + migratorInstance.displayName, + "Selector should show display name" + ); + let profileName = selector.querySelector("#profile-name"); + + if (migratorInstance.profile) { + ok(!isHidden(profileName), "Profile name element should be displayed."); + is( + profileName.textContent, + migratorInstance.profile.name, + "Selector should show profile name" + ); + } else { + ok(isHidden(profileName), "Profile name element should be hidden."); + is(profileName.textContent, ""); + } + + for (let resourceType of getChoosableResourceTypes()) { + let node = resourceTypeList.querySelector( + `label[data-resource-type="${resourceType}"]` + ); + + if (migratorInstance.resourceTypes.includes(resourceType)) { + ok(!isHidden(node), `Selection for ${resourceType} should be shown.`); + ok( + node.control.checked, + `Checkbox for ${resourceType} should be checked.` + ); + } else { + ok(isHidden(node), `Selection for ${resourceType} should be hidden.`); + ok( + !node.control.checked, + `Checkbox for ${resourceType} should be unchecked.` + ); + } + } + } + + let selectAll = gShadowRoot.querySelector("#select-all"); + let summary = gShadowRoot.querySelector("summary"); + ok(isHidden(selectAll), "Selection for select-all should be hidden."); + ok(isHidden(summary), "Summary should be hidden."); + ok(!isHidden(details), "Details should be shown."); + }); + + /** + * Tests the migration wizard with no resources + */ + add_task(async function test_no_resources() { + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: [{ + key: "some-browser-0", + type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER, + displayName: "Some Browser 0 with no resources", + resourceTypes: [], + profile: { id: "person-1", name: "Person 1" }, + hasPermissions: true, + }], + showImportAll: false, + }); + + let noResourcesFound = gShadowRoot.querySelector(".no-resources-found"); + let hideOnErrorEls = gShadowRoot.querySelectorAll(".hide-on-error"); + ok( + !isHidden(noResourcesFound), + "Error message of no reasources should be shown." + ); + for (let hideOnErrorEl of hideOnErrorEls) { + ok(isHidden(hideOnErrorEl), "Item should be hidden."); + } + }); + + /** + * Tests variant 2 of the migration wizard + */ + add_task(async function test_selection_variant_2() { + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: MIGRATOR_PROFILE_INSTANCES, + showImportAll: true, + }); + + let preamble = gShadowRoot.querySelector(".resource-selection-preamble"); + ok(isHidden(preamble), "preamble should be hidden."); + + let selector = gShadowRoot.querySelector("#browser-profile-selector"); + selector.click(); + await new Promise(resolve => { + let panelList = gShadowRoot.querySelector("panel-list"); + if (panelList) { + panelList.addEventListener("shown", resolve, { once: true }); + } + }); + + let panelItems = gShadowRoot.querySelectorAll("panel-list > panel-item"); + is(panelItems.length, 2, "Should have two panel items"); + + let details = gShadowRoot.querySelector("details"); + ok(!details.open, "Details should be closed"); + details.open = true; + + for (let i = 0; i < panelItems.length; i++) { + let migratorInstance = MIGRATOR_PROFILE_INSTANCES[i]; + let panelItem = panelItems[i]; + panelItem.click(); + for (let resourceType of getChoosableResourceTypes()) { + let node = gShadowRoot.querySelector( + `#resource-type-list label[data-resource-type="${resourceType}"]` + ); + if (migratorInstance.resourceTypes.includes(resourceType)) { + ok(!isHidden(node), `Selection for ${resourceType} should be shown.`); + ok( + node.control.checked, + `Checkbox for ${resourceType} should be checked.` + ); + } else { + ok(isHidden(node), `Selection for ${resourceType} should be hidden.`); + ok( + !node.control.checked, + `Checkbox for ${resourceType} should be unchecked.` + ); + } + } + } + + let selectAll = gShadowRoot.querySelector("#select-all"); + let summary = gShadowRoot.querySelector("summary"); + ok(!isHidden(selectAll), "Selection for select-all should be shown."); + ok(selectAll.control.checked, "Checkbox for select-all should be checked."); + ok(!isHidden(summary), "Summary should be shown."); + ok(!isHidden(details), "Details should be shown."); + + let selectAllCheckbox = gShadowRoot.querySelector(".select-all-checkbox"); + selectAllCheckbox.checked = true; + selectAllCheckbox.dispatchEvent(new CustomEvent("change")); + let resourceLabels = gShadowRoot.querySelectorAll("label[data-resource-type]"); + for (let resourceLabel of resourceLabels) { + if (resourceLabel.hidden) { + ok( + !resourceLabel.control.checked, + `Hidden checkbox for ${resourceLabel.dataset.resourceType} should be unchecked.` + + ); + } else { + ok( + resourceLabel.control.checked, + `Visible checkbox for ${resourceLabel.dataset.resourceType} should be checked.` + ); + } + } + + let selectedDataHeader = gShadowRoot.querySelector(".selected-data-header"); + let selectedData = gShadowRoot.querySelector(".selected-data"); + + let bookmarks = gShadowRoot.querySelector("#bookmarks"); + let history = gShadowRoot.querySelector("#history"); + + let selectedDataUpdated = BrowserTestUtils.waitForEvent( + gWiz, + "MigrationWizard:ResourcesUpdated" + ); + bookmarks.control.checked = true; + history.control.checked = true; + bookmarks.dispatchEvent(new CustomEvent("change")); + + ok(bookmarks.control.checked, "Bookmarks should be checked"); + ok(history.control.checked, "History should be checked"); + + await selectedDataUpdated; + + is( + selectedData.textContent, + "Bookmarks and history", + "Testing if selected-data reflects the selected resources." + ); + + is( + selectedDataHeader.dataset.l10nId, + "migration-all-available-data-label", + "Testing if selected-data-header reflects the selected resources" + ); + + let importButton = gShadowRoot.querySelector("#import"); + + ok( + !importButton.disabled, + "Testing if import button is enabled when at least one resource is selected." + ); + + let importButtonUpdated = BrowserTestUtils.waitForEvent( + gWiz, + "MigrationWizard:ResourcesUpdated" + ); + + selectAllCheckbox.checked = false; + selectAllCheckbox.dispatchEvent(new CustomEvent("change", { bubbles: true })); + await importButtonUpdated; + + ok( + importButton.disabled, + "Testing if import button is disabled when no resources are selected." + ); + }); + + /** + * Tests variant 2 of the migration wizard when there's a single resource + * item. + */ + add_task(async function test_selection_variant_2_single_item() { + let resourcesUpdated = BrowserTestUtils.waitForEvent( + gWiz, + "MigrationWizard:ResourcesUpdated" + ); + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: [{ + key: "some-browser-0", + type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER, + displayName: "Some Browser 0 with a single resource", + resourceTypes: ["HISTORY"], + profile: { id: "person-1", name: "Person 1" }, + hasPermissions: true, + }, { + key: "some-browser-1", + type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER, + displayName: "Some Browser 1 with a two resources", + resourceTypes: ["HISTORY", "BOOKMARKS"], + profile: { id: "person-2", name: "Person 2" }, + hasPermissions: true, + }], + showImportAll: true, + }); + await resourcesUpdated; + + let selectAll = gShadowRoot.querySelector("#select-all"); + let summary = gShadowRoot.querySelector("summary"); + let details = gShadowRoot.querySelector("details"); + ok(!details.open, "Details should be closed"); + details.open = true; + + ok(isHidden(selectAll), "Selection for select-all should be hidden."); + ok(!isHidden(summary), "Summary should be shown."); + ok(!isHidden(details), "Details should be shown."); + + resourcesUpdated = BrowserTestUtils.waitForEvent( + gWiz, + "MigrationWizard:ResourcesUpdated" + ); + let browser1Item = gShadowRoot.querySelector("panel-item[key='some-browser-1']"); + browser1Item.click(); + await resourcesUpdated; + + ok(!isHidden(selectAll), "Selection for select-all should be shown."); + ok(!isHidden(summary), "Summary should be shown."); + ok(!isHidden(details), "Details should be shown."); + }); + + /** + * Tests that the Select All checkbox is checked if all non-hidden resource + * types are checked, and unchecked otherwise. + */ + add_task(async function test_selection_variant_2_select_all() { + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: MIGRATOR_PROFILE_INSTANCES, + showImportAll: true, + }); + + let details = gShadowRoot.querySelector("details"); + ok(!details.open, "Details should be closed"); + details.open = true; + + let selectAll = gShadowRoot.querySelector("#select-all"); + ok(selectAll.control.checked, "Select all should be checked by default"); + + let bookmarksResourceLabel = gShadowRoot.querySelector( + "label[data-resource-type='BOOKMARKS']" + ); + ok(bookmarksResourceLabel.control.checked, "Bookmarks should be checked"); + + bookmarksResourceLabel.control.click(); + ok(!bookmarksResourceLabel.control.checked, "Bookmarks should no longer be checked"); + ok(!selectAll.control.checked, "Select all should not longer be checked"); + + bookmarksResourceLabel.control.click(); + ok(bookmarksResourceLabel.control.checked, "Bookmarks should be checked again"); + ok(selectAll.control.checked, "Select all should be checked"); + }); + + /** + * Tests that the wizard can show partial progress during migration. + */ + add_task(async function test_partial_progress() { + const BOOKMARKS_SUCCESS_STRING = "Some bookmarks success string"; + gWiz.setState({ + page: MigrationWizardConstants.PAGES.PROGRESS, + key: "chrome", + progress: { + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS]: { + value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS, + message: BOOKMARKS_SUCCESS_STRING, + }, + // Don't include PASSWORDS to check that it's hidden. + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY]: { + value: MigrationWizardConstants.PROGRESS_VALUE.LOADING, + }, + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS]: { + value: MigrationWizardConstants.PROGRESS_VALUE.LOADING, + }, + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA]: { + value: MigrationWizardConstants.PROGRESS_VALUE.LOADING, + }, + }, + }); + is( + gDeck.selectedViewName, + "page-progress", + "Should have the progress page selected" + ); + + // Bookmarks + let bookmarksGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS + ); + ok(!isHidden(bookmarksGroup), "Bookmarks group should be visible"); + let progressIcon = bookmarksGroup.querySelector(".progress-icon"); + is( + progressIcon.getAttribute("state"), + "success", + "Progress should be completed" + ); + is( + bookmarksGroup.querySelector(".message-text").textContent, + BOOKMARKS_SUCCESS_STRING + ); + + // Passwords + let passwordsGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS + ); + ok(isHidden(passwordsGroup), "Passwords group should be hidden"); + + // History + let historyGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY + ); + ok(!isHidden(historyGroup), "History group should be visible"); + progressIcon = historyGroup.querySelector(".progress-icon"); + is( + progressIcon.getAttribute("state"), + "loading", + "Progress should be still be underway" + ); + is(historyGroup.querySelector(".message-text").textContent.trim(), ""); + + // Extensions + let extensionsGroup = getResourceGroup(MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS); + ok(!isHidden(extensionsGroup), "Extensions group should be visible"); + progressIcon = extensionsGroup.querySelector(".progress-icon"); + is( + progressIcon.getAttribute("state"), + "loading", + "Progress should be still be underway" + ); + is(extensionsGroup.querySelector("a.message-text").textContent.trim(), ""); + is(extensionsGroup.querySelector("span.message-text").textContent.trim(), ""); + + // Form Data + let formDataGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA + ); + ok(!isHidden(formDataGroup), "Form data group should be visible"); + progressIcon = formDataGroup.querySelector(".progress-icon"); + is( + progressIcon.getAttribute("state"), + "loading", + "Progress should be still be underway" + ); + is(formDataGroup.querySelector(".message-text").textContent.trim(), ""); + + // With progress still being underway, the header should be using the + // in progress string. + let header = gShadowRoot.querySelector("#progress-header"); + is( + header.getAttribute("data-l10n-id"), + "migration-wizard-progress-header", + "Should be showing in-progress header string" + ); + + let progressPage = gShadowRoot.querySelector("div[name='page-progress']"); + let doneButton = progressPage.querySelector(".done-button"); + ok(isHidden(doneButton), "Done button should be hidden"); + let cancelButton = progressPage.querySelector(".cancel-close"); + ok(!isHidden(cancelButton), "Cancel button should be visible"); + ok(cancelButton.disabled, "Cancel button should be disabled"); + }); + + /** + * Tests that the wizard can show completed migration progress. + */ + add_task(async function test_completed_progress() { + const BOOKMARKS_SUCCESS_STRING = "Some bookmarks success string"; + const PASSWORDS_SUCCESS_STRING = "Some passwords success string"; + const FORMDATA_SUCCESS_STRING = "Some formdata string"; + const EXTENSIONS_SUCCESS_STRING = "Some extensions string"; + const EXTENSIONS_SUCCESS_HREF = "about:addons"; + gWiz.setState({ + page: MigrationWizardConstants.PAGES.PROGRESS, + key: "chrome", + progress: { + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS]: { + value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS, + message: BOOKMARKS_SUCCESS_STRING, + }, + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS]: { + value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS, + message: PASSWORDS_SUCCESS_STRING, + }, + // Don't include HISTORY to check that it's hidden. + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS]: { + value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS, + message: EXTENSIONS_SUCCESS_STRING, + }, + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA]: { + value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS, + message: FORMDATA_SUCCESS_STRING, + }, + }, + }); + is( + gDeck.selectedViewName, + "page-progress", + "Should have the progress page selected" + ); + + // Bookmarks + let bookmarksGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS + ); + ok(!isHidden(bookmarksGroup), "Bookmarks group should be visible"); + let progressIcon = bookmarksGroup.querySelector(".progress-icon"); + is( + progressIcon.getAttribute("state"), + "success", + "Progress should be completed" + ); + is( + bookmarksGroup.querySelector(".message-text").textContent, + BOOKMARKS_SUCCESS_STRING + ); + + // Passwords + let passwordsGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS + ); + ok(!isHidden(passwordsGroup), "Passwords group should be visible"); + progressIcon = passwordsGroup.querySelector(".progress-icon"); + is( + progressIcon.getAttribute("state"), + "success", + "Progress should be completed" + ); + is( + passwordsGroup.querySelector(".message-text").textContent, + PASSWORDS_SUCCESS_STRING + ); + + // History + let historyGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY + ); + ok(isHidden(historyGroup), "History group should be hidden"); + + // Extensions + let extensionsDataGroup = getResourceGroup(MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS); + ok(!isHidden(extensionsDataGroup), "Extensions data group should be visible"); + progressIcon = extensionsDataGroup.querySelector(".progress-icon"); + is( + progressIcon.getAttribute("state"), + "success", + "Progress should be completed" + ); + is( + extensionsDataGroup.querySelector("a.message-text").textContent, + EXTENSIONS_SUCCESS_STRING + ); + is( + extensionsDataGroup.querySelector("a.message-text").href, + EXTENSIONS_SUCCESS_HREF + ) + // Form Data + let formDataGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA + ); + ok(!isHidden(formDataGroup), "Form data group should be visible"); + progressIcon = formDataGroup.querySelector(".progress-icon"); + is( + progressIcon.getAttribute("state"), + "success", + "Progress should be completed" + ); + is( + formDataGroup.querySelector(".message-text").textContent, + FORMDATA_SUCCESS_STRING + ); + + // With progress being complete, the header should be using the completed + // migration string. + let header = gShadowRoot.querySelector("#progress-header"); + is( + header.getAttribute("data-l10n-id"), + "migration-wizard-progress-done-header", + "Should be showing completed migration header string" + ); + + let progressPage = gShadowRoot.querySelector("div[name='page-progress']"); + let doneButton = progressPage.querySelector(".done-button"); + ok(!isHidden(doneButton), "Done button should be visible and enabled"); + let cancelButton = progressPage.querySelector(".cancel-close"); + ok(isHidden(cancelButton), "Cancel button should be hidden"); + }); + + add_task(async function test_extension_partial_success() { + const EXTENSIONS_INFO_STRING = "Extensions info string"; + const EXTENSIONS_INFO_HREF = "about:addons"; + const EXTENSIONS_SUPPORT_STRING = "extensions support string"; + const EXTENSIONS_SUPPORT_HREF = "about:blank"; + + gWiz.setState({ + page: MigrationWizardConstants.PAGES.PROGRESS, + key: "chrome", + progress: { + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS]: { + value: MigrationWizardConstants.PROGRESS_VALUE.INFO, + message: EXTENSIONS_INFO_STRING, + linkText: EXTENSIONS_SUPPORT_STRING, + linkURL: EXTENSIONS_SUPPORT_HREF, + } + } + }); + is( + gDeck.selectedViewName, + "page-progress", + "Should have the progress page selected" + ); + + let extensionsGroup = getResourceGroup(MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS); + ok(!isHidden(extensionsGroup), "Extensions group should be visible"); + let progressIcon = extensionsGroup.querySelector(".progress-icon"); + is( + progressIcon.getAttribute("state"), + "info", + "Progress should be completed, in info state" + ); + is( + extensionsGroup.querySelector("a.message-text").textContent, + EXTENSIONS_INFO_STRING + ); + is( + extensionsGroup.querySelector("a.message-text").href, + EXTENSIONS_INFO_HREF + ); + is( + extensionsGroup.querySelector(".support-text").textContent, + EXTENSIONS_SUPPORT_STRING + ); + is( + extensionsGroup.querySelector(".support-text").href, + EXTENSIONS_SUPPORT_HREF + ); + + // With progress being complete, the header should be using the completed + // migration string. + let header = gShadowRoot.querySelector("#progress-header"); + is( + header.getAttribute("data-l10n-id"), + "migration-wizard-progress-done-header", + "Should be showing completed migration header string" + ); + + let progressPage = gShadowRoot.querySelector("div[name='page-progress']"); + let doneButton = progressPage.querySelector(".done-button"); + ok(!isHidden(doneButton), "Done button should be visible and enabled"); + let cancelButton = progressPage.querySelector(".cancel-close"); + ok(isHidden(cancelButton), "Cancel button should be hidden"); + }); + + add_task(async function test_extension_error() { + const EXTENSIONS_ERROR_STRING = "Extensions error string"; + const EXTENSIONS_SUPPORT_STRING = "extensions support string"; + const EXTENSIONS_SUPPORT_HREF = "about:blank"; + + gWiz.setState({ + page: MigrationWizardConstants.PAGES.PROGRESS, + key: "chrome", + progress: { + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS]: { + value: MigrationWizardConstants.PROGRESS_VALUE.WARNING, + message: EXTENSIONS_ERROR_STRING, + linkText: EXTENSIONS_SUPPORT_STRING, + linkURL: EXTENSIONS_SUPPORT_HREF, + } + } + }); + is( + gDeck.selectedViewName, + "page-progress", + "Should have the progress page selected" + ); + + let extensionsGroup = getResourceGroup(MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS); + ok(!isHidden(extensionsGroup), "Extensions group should be visible"); + let progressIcon = extensionsGroup.querySelector(".progress-icon"); + is(progressIcon.getAttribute("state"), "warning"); + is( + extensionsGroup.querySelector("a.message-text").textContent, + "" + ); + is( + extensionsGroup.querySelector("span.message-text").textContent, + EXTENSIONS_ERROR_STRING + ); + is( + extensionsGroup.querySelector(".support-text").textContent, + EXTENSIONS_SUPPORT_STRING + ); + is( + extensionsGroup.querySelector(".support-text").href, + EXTENSIONS_SUPPORT_HREF + ); + + // With progress being complete, the header should be using the completed + // migration string. + let header = gShadowRoot.querySelector("#progress-header"); + is( + header.getAttribute("data-l10n-id"), + "migration-wizard-progress-done-with-warnings-header", + "Should be showing completed migration header string" + ); + + let progressPage = gShadowRoot.querySelector("div[name='page-progress']"); + let doneButton = progressPage.querySelector(".done-button"); + ok(!isHidden(doneButton), "Done button should be visible and enabled"); + let cancelButton = progressPage.querySelector(".cancel-close"); + ok(isHidden(cancelButton), "Cancel button should be hidden"); + }); + + /** + * Tests that the wizard can show partial progress during file migration. + */ + add_task(async function test_partial_file_progress() { + const PASSWORDS_SUCCESS_STRING = "Some passwords success string"; + const TITLE = "Partial progress"; + + gWiz.setState({ + page: MigrationWizardConstants.PAGES.FILE_IMPORT_PROGRESS, + title: "Partial progress", + progress: { + [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_FROM_FILE]: { + value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS, + message: PASSWORDS_SUCCESS_STRING, + }, + [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW]: { + value: MigrationWizardConstants.PROGRESS_VALUE.LOADING, + }, + }, + }); + + is( + gDeck.selectedViewName, + "page-file-import-progress", + "Should have the file progress page selected" + ); + + let header = gShadowRoot.querySelector("#file-import-progress-header"); + is(header.textContent, TITLE, "Title is set correctly."); + + let passwordsFromFileGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_FROM_FILE + ); + ok(!isHidden(passwordsFromFileGroup), "Passwords from file group should be visible"); + let progressIcon = passwordsFromFileGroup.querySelector(".progress-icon"); + is( + progressIcon.getAttribute("state"), + "success", + "Progress should be completed" + ); + is( + passwordsFromFileGroup.querySelector(".message-text").textContent, + PASSWORDS_SUCCESS_STRING + ); + + let passwordsUpdatedGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_UPDATED + ); + ok(isHidden(passwordsUpdatedGroup), "Passwords updated group should be hidden"); + + let passwordsNewGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW + ); + ok(!isHidden(passwordsNewGroup), "Passwords new group should be visible"); + progressIcon = passwordsNewGroup.querySelector(".progress-icon"); + is( + progressIcon.getAttribute("state"), + "loading", + "Progress should be still be underway" + ); + is(passwordsNewGroup.querySelector(".message-text").textContent.trim(), ""); + + let progressPage = gShadowRoot.querySelector("div[name='page-file-import-progress']"); + let doneButton = progressPage.querySelector(".done-button"); + ok(isHidden(doneButton), "Done button should be hidden"); + let cancelButton = progressPage.querySelector(".cancel-close"); + ok(!isHidden(cancelButton), "Cancel button should be visible"); + ok(cancelButton.disabled, "Cancel button should be disabled"); + }); + + /** + * Tests that the wizard can show completed migration progress. + */ + add_task(async function test_completed_file_progress() { + const PASSWORDS_NEW_SUCCESS_STRING = "2 added"; + const PASSWORDS_UPDATED_SUCCESS_STRING = "5 updated"; + const TITLE = "Done doing file import"; + + gWiz.setState({ + page: MigrationWizardConstants.PAGES.FILE_IMPORT_PROGRESS, + title: TITLE, + progress: { + [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW]: { + value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS, + message: PASSWORDS_NEW_SUCCESS_STRING, + }, + [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_UPDATED]: { + value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS, + message: PASSWORDS_UPDATED_SUCCESS_STRING, + }, + }, + }); + is( + gDeck.selectedViewName, + "page-file-import-progress", + "Should have the file progress page selected" + ); + + let header = gShadowRoot.querySelector("#file-import-progress-header"); + is(header.textContent, TITLE, "Title is set correctly."); + + let passwordsNewGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW + ); + ok(!isHidden(passwordsNewGroup), "Passwords new group should be visible"); + let progressIcon = passwordsNewGroup.querySelector(".progress-icon"); + is( + progressIcon.getAttribute("state"), + "success", + "Progress should be completed" + ); + is( + passwordsNewGroup.querySelector(".message-text").textContent, + PASSWORDS_NEW_SUCCESS_STRING + ); + + let passwordsUpdatedGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_UPDATED + ); + ok(!isHidden(passwordsUpdatedGroup), "Passwords updated group should be visible"); + progressIcon = passwordsUpdatedGroup.querySelector(".progress-icon"); + is( + progressIcon.getAttribute("state"), + "success", + "Progress should be completed" + ); + is( + passwordsUpdatedGroup.querySelector(".message-text").textContent, + PASSWORDS_UPDATED_SUCCESS_STRING + ); + + let passwordsFromFileGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_FROM_FILE + ); + ok(isHidden(passwordsFromFileGroup), "Passwords from file group should be hidden"); + + let progressPage = gShadowRoot.querySelector("div[name='page-file-import-progress']"); + let doneButton = progressPage.querySelector(".done-button"); + ok(!isHidden(doneButton), "Done button should be visible and enabled"); + let cancelButton = progressPage.querySelector(".cancel-close"); + ok(isHidden(cancelButton), "Cancel button should be hidden"); + }); + + /** + * Tests that the buttons that dismiss the wizard when embedded in + * a dialog are only visible when in dialog mode, and dispatch a + * MigrationWizard:Close event when clicked. + */ + add_task(async function test_dialog_mode_close() { + gWiz.toggleAttribute("dialog-mode", true); + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: MIGRATOR_PROFILE_INSTANCES, + }); + + // For now, there's only a single .cancel-close button, so let's just test + // that one. Let's make this test fail if there are multiple so that we can + // then update this test to switch to the right pages to test those buttons + // too. + let buttons = gShadowRoot.querySelectorAll(".cancel-close:not([disabled])"); + ok( + buttons.length, + "This test expects at least one enabled .cancel-close button" + ); + let button = buttons[0]; + ok( + !isHidden(button), + ".cancel-close button should be visible in dialog mode." + ); + let closeEvent = BrowserTestUtils.waitForEvent(gWiz, "MigrationWizard:Close"); + button.click(); + await closeEvent; + + gWiz.toggleAttribute("dialog-mode", false); + ok( + isHidden(button), + ".cancel-close button should be hidden when not in dialog mode." + ); + }); + + /** + * Internet Explorer and Edge refer to bookmarks as "favorites", + * and we change our labels to suit when either of those browsers are + * selected as the migration source. This test tests that behavior in the + * selection page. + */ + add_task(async function test_ie_edge_favorites_selection() { + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: MIGRATOR_PROFILE_INSTANCES, + showImportAll: false, + }); + + let bookmarksCheckboxLabel = gShadowRoot.querySelector("#bookmarks"); + let span = bookmarksCheckboxLabel.querySelector("span[default-data-l10n-id]"); + ok(span, "The bookmarks selection span has a default-data-l10n-id attribute"); + is( + span.getAttribute("data-l10n-id"), + span.getAttribute("default-data-l10n-id"), + "Should be showing the default string for bookmarks" + ); + + // Now test when in Variant 2, for the string in the <summary>. + let selectedDataUpdated = BrowserTestUtils.waitForEvent( + gWiz, + "MigrationWizard:ResourcesUpdated" + ); + + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: MIGRATOR_PROFILE_INSTANCES, + showImportAll: true, + }); + + await selectedDataUpdated; + + let summary = gShadowRoot.querySelector("summary"); + ok( + summary.textContent.toLowerCase().includes("bookmarks"), + "Summary should include the string 'bookmarks'" + ); + + for (let key of MigrationWizardConstants.USES_FAVORITES) { + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: [{ + key, + displayName: "Legacy Microsoft Browser", + resourceTypes: ["BOOKMARKS"], + profile: null, + hasPermissions: true, + }], + showImportAll: false, + }); + + is( + span.getAttribute("data-l10n-id"), + span.getAttribute("ie-edge-data-l10n-id"), + "Should be showing the IE/Edge string for bookmarks" + ); + + // Now test when in Variant 2, for the string in the <summary>. + selectedDataUpdated = BrowserTestUtils.waitForEvent( + gWiz, + "MigrationWizard:ResourcesUpdated" + ); + + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: [{ + key, + displayName: "Legacy Microsoft Browser", + resourceTypes: ["BOOKMARKS"], + profile: null, + hasPermissions: true, + }], + showImportAll: true, + }); + + await selectedDataUpdated; + + ok( + summary.textContent.toLowerCase().includes("favorites"), + "Summary should include the string 'favorites'" + ); + } + }); + + /** + * Internet Explorer and Edge refer to bookmarks as "favorites", + * and we change our labels to suit when either of those browsers are + * selected as the migration source. This test tests that behavior in the + * progress page + */ + add_task(async function test_ie_edge_favorites_progress() { + gWiz.setState({ + page: MigrationWizardConstants.PAGES.PROGRESS, + key: "chrome", + progress: { + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS]: { + inProgress: false, + message: "A string from the parent", + }, + }, + }); + + let bookmarksGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS + ); + let span = bookmarksGroup.querySelector("span[default-data-l10n-id]"); + ok(span, "Should have found a span with default-data-l10n-id"); + is( + span.getAttribute("data-l10n-id"), + span.getAttribute("default-data-l10n-id"), + "Should be using the default string." + ); + + + for (let key of MigrationWizardConstants.USES_FAVORITES) { + gWiz.setState({ + page: MigrationWizardConstants.PAGES.PROGRESS, + key, + progress: { + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS]: { + inProgress: false, + message: "A string from the parent", + }, + }, + }); + + is( + span.getAttribute("data-l10n-id"), + span.getAttribute("ie-edge-data-l10n-id"), + "Should be showing the IE/Edge string for bookmarks" + ); + } + }); + + /** + * Tests that the button shown in either the browser migration success or + * file migration success pages is a "continue" button rather than a + * "done" button. + */ + add_task(async function test_embedded_continue_button() { + gWiz.toggleAttribute("dialog-mode", false); + gWiz.setState({ + page: MigrationWizardConstants.PAGES.PROGRESS, + key: "chrome", + progress: { + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS]: { + value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS, + message: "A string from the parent", + }, + }, + }); + + let progressPage = gShadowRoot.querySelector("div[name='page-progress']"); + let cancelButton = progressPage.querySelector(".cancel-close"); + ok(isHidden(cancelButton), "Cancel button should be hidden"); + let doneButton = progressPage.querySelector(".done-button"); + ok(isHidden(doneButton), "Done button should be hidden when embedding"); + let continueButton = progressPage.querySelector(".continue-button"); + ok(!isHidden(continueButton), "Continue button should be displayed"); + + let content = document.getElementById("content"); + + let promise = new Promise(resolve => { + content.addEventListener("MigrationWizard:Close", resolve, { + once: true, + }); + }); + continueButton.click(); + await promise; + ok( + true, + "Clicking on the Continue button sent the MigrationWizard:Close event." + ); + + gWiz.setState({ + page: MigrationWizardConstants.PAGES.FILE_IMPORT_PROGRESS, + title: "Done importing file data", + progress: { + [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW]: { + inProgress: false, + message: "Some message", + }, + [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_UPDATED]: { + inProgress: false, + message: "Some other", + }, + }, + }); + + progressPage = gShadowRoot.querySelector("div[name='page-file-import-progress']"); + cancelButton = progressPage.querySelector(".cancel-close"); + ok(isHidden(cancelButton), "Cancel button should be hidden"); + doneButton = progressPage.querySelector(".done-button"); + ok(isHidden(doneButton), "Done button should be hidden when embedding"); + continueButton = progressPage.querySelector(".continue-button"); + ok(!isHidden(continueButton), "Continue button should be displayed"); + + promise = new Promise(resolve => { + content.addEventListener("MigrationWizard:Close", resolve, { + once: true, + }); + }); + continueButton.click(); + await promise; + ok( + true, + "Clicking on the Continue button sent the MigrationWizard:Close event." + ); + + gWiz.toggleAttribute("dialog-mode", true); + }); + + /** + * Tests that if a progress update comes down which puts a resource from + * being done to loading, that the status message is cleared. + */ + add_task(async function test_clear_status_message_when_in_progress() { + const STRING_TO_CLEAR = "This string should get cleared"; + gWiz.toggleAttribute("dialog-mode", false); + gWiz.setState({ + page: MigrationWizardConstants.PAGES.PROGRESS, + key: "chrome", + progress: { + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS]: { + value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS, + message: STRING_TO_CLEAR, + }, + }, + }); + + let bookmarksGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS + ); + ok(!isHidden(bookmarksGroup), "Bookmarks group should be visible"); + let progressIcon = bookmarksGroup.querySelector(".progress-icon"); + is( + progressIcon.getAttribute("state"), + "success", + "Progress should be completed" + ); + is( + bookmarksGroup.querySelector(".message-text").textContent, + STRING_TO_CLEAR + ); + + gWiz.setState({ + page: MigrationWizardConstants.PAGES.PROGRESS, + key: "chrome", + progress: { + [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS]: { + value: MigrationWizardConstants.PROGRESS_VALUE.LOADING, + message: "", + }, + }, + }); + + ok(!isHidden(bookmarksGroup), "Bookmarks group should be visible"); + is( + progressIcon.getAttribute("state"), + "loading", + "Progress should be still be underway" + ); + is( + bookmarksGroup.querySelector(".message-text").textContent.trim(), + "" + ); + }); + + /** + * Tests that if a file progress update comes down which puts a resource + * from being done to loading, that the status message is cleared. + * + * This is extremely similar to the above test, except that it's for progress + * updates for file resources. + */ + add_task(async function test_clear_status_message_when_in_file_progress() { + const STRING_TO_CLEAR = "This string should get cleared"; + gWiz.toggleAttribute("dialog-mode", false); + gWiz.setState({ + page: MigrationWizardConstants.PAGES.FILE_IMPORT_PROGRESS, + key: "chrome", + progress: { + [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW]: { + value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS, + message: STRING_TO_CLEAR, + }, + }, + }); + + let passwordsGroup = getResourceGroup( + MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW + ); + ok(!isHidden(passwordsGroup), "Passwords group should be visible"); + let progressIcon = passwordsGroup.querySelector(".progress-icon"); + is( + progressIcon.getAttribute("state"), + "success", + "Progress should be completed" + ); + is( + passwordsGroup.querySelector(".message-text").textContent, + STRING_TO_CLEAR + ); + + gWiz.setState({ + page: MigrationWizardConstants.PAGES.FILE_IMPORT_PROGRESS, + key: "chrome", + progress: { + [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW]: { + value: MigrationWizardConstants.PROGRESS_VALUE.LOADING, + message: "", + }, + }, + }); + + ok(!isHidden(passwordsGroup), "Passwords group should be visible"); + is( + progressIcon.getAttribute("state"), + "loading", + "Progress should be still be underway" + ); + is( + passwordsGroup.querySelector(".message-text").textContent.trim(), + "" + ); + }); + + /** + * Tests that the migration wizard can be put into the selection page after + * a file migrator error and show an error message. + */ + add_task(async function test_file_migrator_error() { + const FILE_MIGRATOR_KEY = "some-file-migrator"; + const FILE_IMPORT_ERROR_MESSAGE = "This is an error message"; + const MIGRATORS = [ + { + key: "some-browser-0", + type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER, + displayName: "Some Browser 0", + resourceTypes: ["HISTORY", "FORMDATA", "PASSWORDS", "BOOKMARKS"], + profile: { id: "person-2", name: "Person 2" }, + hasPermissions: true, + }, + { + key: "some-browser-1", + type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER, + displayName: "Some Browser 1", + resourceTypes: ["HISTORY", "BOOKMARKS"], + profile: null, + hasPermissions: true, + }, + { + key: FILE_MIGRATOR_KEY, + type: MigrationWizardConstants.MIGRATOR_TYPES.FILE, + displayName: "Some File Migrator", + resourceTypes: [], + }, + ]; + + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: MIGRATORS, + showImportAll: true, + migratorKey: "some-file-migrator", + fileImportErrorMessage: FILE_IMPORT_ERROR_MESSAGE, + }); + + let errorMessageContainer = gShadowRoot.querySelector(".file-import-error"); + ok(!isHidden(errorMessageContainer), "Error message should be shown."); + let errorText = gShadowRoot.querySelector("#file-import-error-message").textContent; + is(errorText, FILE_IMPORT_ERROR_MESSAGE); + + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: MIGRATORS, + showImportAll: true, + }); + + ok(isHidden(errorMessageContainer), "Error message should be hidden."); + errorText = gShadowRoot.querySelector("#file-import-error-message").textContent; + is(errorText, ""); + }); + + /** + * Tests that the variant of the wizard can be forced via + * attributes on the migration-wizard element. + */ + add_task(async function force_show_import_all() { + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: MIGRATOR_PROFILE_INSTANCES, + showImportAll: true, + }); + + let selectionPage = gShadowRoot.querySelector("div[name='page-selection']"); + let details = gShadowRoot.querySelector("details"); + ok( + selectionPage.hasAttribute("show-import-all"), + "Should be paying attention to showImportAll=true on state object" + ); + ok(!details.open, "Details collapsed by default"); + + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: MIGRATOR_PROFILE_INSTANCES, + showImportAll: false, + }); + + ok( + !selectionPage.hasAttribute("show-import-all"), + "Should be paying attention to showImportAll=false on state object" + ); + ok(details.open, "Details expanded by default"); + + gWiz.setAttribute("force-show-import-all", "false"); + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: MIGRATOR_PROFILE_INSTANCES, + showImportAll: true, + }); + ok( + !selectionPage.hasAttribute("show-import-all"), + "Should be paying attention to force-show-import-all=false on DOM node" + ); + ok(details.open, "Details expanded by default"); + + gWiz.setAttribute("force-show-import-all", "true"); + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: MIGRATOR_PROFILE_INSTANCES, + showImportAll: false, + }); + ok( + selectionPage.hasAttribute("show-import-all"), + "Should be paying attention to force-show-import-all=true on DOM node" + ); + ok(!details.open, "Details collapsed by default"); + + gWiz.removeAttribute("force-show-import-all"); + }); + + /** + * Tests that for non-Safari migrators without permissions, we show + * the appropriate message and the button for getting permissions. + */ + add_task(async function no_permissions() { + const SOME_FAKE_PERMISSION_PATH = "/some/fake/path"; + + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: [{ + key: "some-browser-0", + type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER, + displayName: "Some Browser 0 with no permissions", + resourceTypes: [], + profile: null, + hasPermissions: false, + permissionsPath: SOME_FAKE_PERMISSION_PATH, + }], + showImportAll: false, + }); + + let selectionPage = gShadowRoot.querySelector("div[name='page-selection']"); + ok(selectionPage.hasAttribute("no-permissions"), "no-permissions attribute set."); + + let resourceList = gShadowRoot.querySelector(".resource-selection-details"); + ok(isHidden(resourceList), "Resources list is hidden."); + + let importButton = gShadowRoot.querySelector("#import"); + ok(isHidden(importButton), "Import button hidden."); + let noPermissionsMessage = gShadowRoot.querySelector(".no-permissions-message"); + ok(!isHidden(noPermissionsMessage), "No permissions message shown."); + let getPermissionButton = gShadowRoot.querySelector("#get-permissions"); + ok(!isHidden(getPermissionButton), "Get permissions button shown."); + + let step2 = gShadowRoot.querySelector(".migration-no-permissions-instructions-step2"); + ok(step2.hasAttribute("data-l10n-args")); + is(JSON.parse(step2.getAttribute("data-l10n-args")).permissionsPath, SOME_FAKE_PERMISSION_PATH); + + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: MIGRATOR_PROFILE_INSTANCES, + showImportAll: false, + }); + + ok(!selectionPage.hasAttribute("no-permissions"), "no-permissions attribute set to false."); + ok(!isHidden(resourceList), "Resources list is shown."); + ok(!isHidden(importButton), "Import button is shown."); + ok(isHidden(noPermissionsMessage), "No permissions message hidden."); + ok(isHidden(getPermissionButton), "Get permissions button hidden."); + }); + + /** + * Tests that for the Safari migrator without permissions, we show the + * normal resources list and impor tbutton instead of the no permissions + * message. Safari has a special flow where permissions are requested + * only after resource selection has occurred. + */ + add_task(async function no_permissions_safari() { + const SOME_FAKE_PERMISSION_PATH = "/some/fake/safari/path"; + + gWiz.setState({ + page: MigrationWizardConstants.PAGES.SELECTION, + migrators: [{ + key: "safari", + type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER, + displayName: "Safari with no permissions", + resourceTypes: ["HISTORY", "BOOKMARKS"], + profile: null, + hasPermissions: false, + permissionsPath: SOME_FAKE_PERMISSION_PATH, + }], + showImportAll: false, + }); + + let selectionPage = gShadowRoot.querySelector("div[name='page-selection']"); + ok(!selectionPage.hasAttribute("no-permissions"), "no-permissions attribute not set."); + + let resourceList = gShadowRoot.querySelector(".resource-selection-details"); + ok(!isHidden(resourceList), "Resources list is shown."); + + let importButton = gShadowRoot.querySelector("#import"); + ok(!isHidden(importButton), "Import button shown."); + let noPermissionsMessage = gShadowRoot.querySelector(".no-permissions-message"); + ok(isHidden(noPermissionsMessage), "No permissions message hidden."); + let getPermissionButton = gShadowRoot.querySelector("#get-permissions"); + ok(isHidden(getPermissionButton), "Get permissions button hiddne."); + }); + </script> + </head> + <body> + <p id="display"></p> + <div id="content"> + <migration-wizard id="test-wizard" dialog-mode=""></migration-wizard> + </div> + <pre id="test"></pre> + </body> +</html> diff --git a/browser/components/migration/tests/head-common.js b/browser/components/migration/tests/head-common.js new file mode 100644 index 0000000000..025d3e5a16 --- /dev/null +++ b/browser/components/migration/tests/head-common.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { MigrationWizardConstants } = ChromeUtils.importESModule( + "chrome://browser/content/migration/migration-wizard-constants.mjs" +); + +/** + * Returns the constant strings from MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES + * that aren't also part of MigrationWizardConstants.PROFILE_RESET_ONLY_RESOURCE_TYPES. + * + * This is the set of resources that the user can actually choose to migrate via + * checkboxes. + * + * @returns {string[]} + */ +function getChoosableResourceTypes() { + return Object.keys(MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES).filter( + resourceType => + !MigrationWizardConstants.PROFILE_RESET_ONLY_RESOURCE_TYPES[resourceType] + ); +} diff --git a/browser/components/migration/tests/marionette/manifest.toml b/browser/components/migration/tests/marionette/manifest.toml new file mode 100644 index 0000000000..1e3b536ee2 --- /dev/null +++ b/browser/components/migration/tests/marionette/manifest.toml @@ -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..ea5d6bce99 --- /dev/null +++ b/browser/components/migration/tests/marionette/test_refresh_firefox.py @@ -0,0 +1,703 @@ +import os +import time + +from marionette_driver.errors import NoAlertPresentException +from marionette_harness import MarionetteTestCase + + +# Holds info about things we need to cleanup after the tests are done. +class PendingCleanup: + desktop_backup_path = None + reset_profile_path = None + reset_profile_local_path = None + + def __init__(self, profile_name_to_remove): + self.profile_name_to_remove = profile_name_to_remove + + +class TestFirefoxRefresh(MarionetteTestCase): + _sandbox = "firefox-refresh" + + _username = "marionette-test-login" + _password = "marionette-test-password" + _bookmarkURL = "about:mozilla" + _bookmarkText = "Some bookmark from Marionette" + + _cookieHost = "firefox-refresh.marionette-test.mozilla.org" + _cookiePath = "some/cookie/path" + _cookieName = "somecookie" + _cookieValue = "some cookie value" + + _historyURL = "http://firefox-refresh.marionette-test.mozilla.org/" + _historyTitle = "Test visit for Firefox Reset" + + _formHistoryFieldName = "some-very-unique-marionette-only-firefox-reset-field" + _formHistoryValue = "special-pumpkin-value" + + _formAutofillAvailable = False + _formAutofillAddressGuid = None + + _expectedURLs = ["about:robots", "about:mozilla"] + + def savePassword(self): + self.runAsyncCode( + """ + let [username, password, resolve] = arguments; + let myLogin = new global.LoginInfo( + "test.marionette.mozilla.com", + "http://test.marionette.mozilla.com/some/form/", + null, + username, + password, + "username", + "password" + ); + Services.logins.addLoginAsync(myLogin) + .then(() => resolve(false), resolve); + """, + script_args=(self._username, self._password), + ) + + def createBookmarkInMenu(self): + error = self.runAsyncCode( + """ + // let url = arguments[0]; + // let title = arguments[1]; + // let resolve = arguments[arguments.length - 1]; + let [url, title, resolve] = arguments; + PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, url, title + }).then(() => resolve(false), resolve); + """, + script_args=(self._bookmarkURL, self._bookmarkText), + ) + if error: + print(error) + + def createBookmarksOnToolbar(self): + error = self.runAsyncCode( + """ + let resolve = arguments[arguments.length - 1]; + let children = []; + for (let i = 1; i <= 5; i++) { + children.push({url: `about:rights?p=${i}`, title: `Bookmark ${i}`}); + } + PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children + }).then(() => resolve(false), resolve); + """ + ) + if error: + print(error) + + def createHistory(self): + error = self.runAsyncCode( + """ + let resolve = arguments[arguments.length - 1]; + PlacesUtils.history.insert({ + url: arguments[0], + title: arguments[1], + visits: [{ + date: new Date(Date.now() - 5000), + referrer: "about:mozilla" + }] + }).then(() => resolve(false), + ex => resolve("Unexpected error in adding visit: " + ex)); + """, + script_args=(self._historyURL, self._historyTitle), + ) + if error: + print(error) + + def createFormHistory(self): + error = self.runAsyncCode( + """ + let updateDefinition = { + op: "add", + fieldname: arguments[0], + value: arguments[1], + firstUsed: (Date.now() - 5000) * 1000, + }; + let resolve = arguments[arguments.length - 1]; + global.FormHistory.update(updateDefinition).then(() => { + resolve(false); + }, error => { + resolve("Unexpected error in adding formhistory: " + error); + }); + """, + script_args=(self._formHistoryFieldName, self._formHistoryValue), + ) + if error: + print(error) + + def createFormAutofill(self): + if not self._formAutofillAvailable: + return + self._formAutofillAddressGuid = self.runAsyncCode( + """ + let resolve = arguments[arguments.length - 1]; + const TEST_ADDRESS_1 = { + "given-name": "John", + "additional-name": "R.", + "family-name": "Smith", + organization: "World Wide Web Consortium", + "street-address": "32 Vassar Street\\\nMIT Room 32-G524", + "address-level2": "Cambridge", + "address-level1": "MA", + "postal-code": "02139", + country: "US", + tel: "+15195555555", + email: "user@example.com", + }; + return global.formAutofillStorage.initialize().then(() => { + return global.formAutofillStorage.addresses.add(TEST_ADDRESS_1); + }).then(resolve); + """ + ) + + def createCookie(self): + self.runCode( + """ + // Expire in 15 minutes: + let expireTime = Math.floor(Date.now() / 1000) + 15 * 60; + Services.cookies.add(arguments[0], arguments[1], arguments[2], arguments[3], + true, false, false, expireTime, {}, + Ci.nsICookie.SAMESITE_NONE, Ci.nsICookie.SCHEME_UNSET); + """, + script_args=( + self._cookieHost, + self._cookiePath, + self._cookieName, + self._cookieValue, + ), + ) + + def createSession(self): + self.runAsyncCode( + """ + let resolve = arguments[arguments.length - 1]; + const COMPLETE_STATE = Ci.nsIWebProgressListener.STATE_STOP + + Ci.nsIWebProgressListener.STATE_IS_NETWORK; + let { TabStateFlusher } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/TabStateFlusher.sys.mjs" + ); + let expectedURLs = Array.from(arguments[0]) + gBrowser.addTabsProgressListener({ + onStateChange(browser, webprogress, request, flags, status) { + try { + request && request.QueryInterface(Ci.nsIChannel); + } catch (ex) {} + let uriLoaded = request.originalURI && request.originalURI.spec; + if ((flags & COMPLETE_STATE == COMPLETE_STATE) && uriLoaded && + expectedURLs.includes(uriLoaded)) { + TabStateFlusher.flush(browser).then(function() { + expectedURLs.splice(expectedURLs.indexOf(uriLoaded), 1); + if (!expectedURLs.length) { + gBrowser.removeTabsProgressListener(this); + resolve(); + } + }); + } + } + }); + let expectedTabs = new Set(); + for (let url of expectedURLs) { + expectedTabs.add(gBrowser.addTab(url, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + })); + } + // Close any other tabs that might be open: + let allTabs = Array.from(gBrowser.tabs); + for (let tab of allTabs) { + if (!expectedTabs.has(tab)) { + gBrowser.removeTab(tab); + } + } + """, # NOQA: E501 + script_args=(self._expectedURLs,), + ) + + def createFxa(self): + # This script will write an entry to the login manager and create + # a signedInUser.json in the profile dir. + self.runAsyncCode( + """ + let resolve = arguments[arguments.length - 1]; + let { FxAccountsStorageManager } = ChromeUtils.import( + "resource://gre/modules/FxAccountsStorage.jsm" + ); + let storage = new FxAccountsStorageManager(); + let data = {email: "test@test.com", uid: "uid", keyFetchToken: "top-secret"}; + storage.initialize(data); + storage.finalize().then(resolve); + """ + ) + + def createSync(self): + # This script will write the canonical preference which indicates a user + # is signed into sync. + self.marionette.execute_script( + """ + Services.prefs.setStringPref("services.sync.username", "test@test.com"); + """ + ) + + def checkPassword(self): + loginInfo = self.runAsyncCode( + """ + let [resolve] = arguments; + Services.logins.searchLoginsAsync({ + origin: "test.marionette.mozilla.com", + formActionOrigin: "http://test.marionette.mozilla.com/some/form/", + }).then(ary => resolve(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.runAsyncCode( + """ + let resolve = arguments[arguments.length - 1]; + Services.logins.getAllLogins().then(logins => resolve(logins.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 checkStartupMigrationStateCleared(self): + result = self.runCode( + """ + let { MigrationUtils } = ChromeUtils.importESModule( + "resource:///modules/MigrationUtils.sys.mjs" + ); + return MigrationUtils.isStartupMigration; + """ + ) + self.assertFalse(result) + + 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() + self.checkStartupMigrationStateCleared() + + 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/Favicons b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Favicons Binary files differnew file mode 100644 index 0000000000..fddee798b3 --- /dev/null +++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Favicons diff --git a/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data Binary files differnew file mode 100644 index 0000000000..7e6e843a03 --- /dev/null +++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data diff --git a/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Web Data b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Web Data Binary files differnew file mode 100644 index 0000000000..c557c9b851 --- /dev/null +++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Web Data 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 Binary files differnew file mode 100644 index 0000000000..fd135624c4 --- /dev/null +++ b/browser/components/migration/tests/unit/AppData/LocalWithNoData/Google/Chrome/User Data/Default/Login Data 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 Binary files differnew file mode 100644 index 0000000000..1835c33583 --- /dev/null +++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat 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 Binary files differnew file mode 100644 index 0000000000..83d855cb33 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookies diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/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..983c37560c --- /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,4 @@ +{ + "default_locale": "en_US", + "name": "Fake Extension 2" +} diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryCorrupt b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryCorrupt Binary files differnew file mode 100644 index 0000000000..8585f308c5 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryCorrupt 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 Binary files differnew file mode 100644 index 0000000000..7fb19903b0 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryMaster 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 Binary files differnew file mode 100644 index 0000000000..19b8542b98 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Login Data 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 Binary files differnew file mode 100644 index 0000000000..a9c33e1b1a --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Bookmarks.plist diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db Binary files differnew file mode 100644 index 0000000000..dd5d0c7512 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-lock b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-lock new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-lock diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-shm b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-shm Binary files differnew file mode 100644 index 0000000000..edd607898b --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-shm diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-wal b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-wal Binary files differnew file mode 100644 index 0000000000..e145119298 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-wal diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/04860FA3D07D8936B87D2B965317C6E9 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/04860FA3D07D8936B87D2B965317C6E9 Binary files differnew file mode 100644 index 0000000000..1c6741c165 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/04860FA3D07D8936B87D2B965317C6E9 diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/19A777E4F7BDA0C0E350D6C681B6E271 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/19A777E4F7BDA0C0E350D6C681B6E271 Binary files differnew file mode 100644 index 0000000000..47b40f707f --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/19A777E4F7BDA0C0E350D6C681B6E271 diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/2558A57A1AE576AA31F0DCD1364B3F42 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/2558A57A1AE576AA31F0DCD1364B3F42 Binary files differnew file mode 100644 index 0000000000..2a4c30b31e --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/2558A57A1AE576AA31F0DCD1364B3F42 diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/57D1907A1EBDA1889AA85B8AB7A90804 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/57D1907A1EBDA1889AA85B8AB7A90804 Binary files differnew file mode 100644 index 0000000000..f4996ba082 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/57D1907A1EBDA1889AA85B8AB7A90804 diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/6EEDD53B65A19CB364EB6FB07DEACF80 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/6EEDD53B65A19CB364EB6FB07DEACF80 Binary files differnew file mode 100644 index 0000000000..f519ce9ad2 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/6EEDD53B65A19CB364EB6FB07DEACF80 diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/7F65370AD319C7B294EDF2E2BEBA880F b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/7F65370AD319C7B294EDF2E2BEBA880F Binary files differnew file mode 100644 index 0000000000..e70021849b --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/7F65370AD319C7B294EDF2E2BEBA880F diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/999E2BD5CD612AA550F222A1088DB3D8 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/999E2BD5CD612AA550F222A1088DB3D8 Binary files differnew file mode 100644 index 0000000000..559502b02b --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/999E2BD5CD612AA550F222A1088DB3D8 diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/9D8A6E2153D42043A7AE0430B41D374A b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/9D8A6E2153D42043A7AE0430B41D374A Binary files differnew file mode 100644 index 0000000000..89ed9a1c39 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/9D8A6E2153D42043A7AE0430B41D374A diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/A21F634481CF5188329FD2052F07ADBC b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/A21F634481CF5188329FD2052F07ADBC Binary files differnew file mode 100644 index 0000000000..7b86185e67 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/A21F634481CF5188329FD2052F07ADBC diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/BC2288B5BA9B7BE352BA586257442E08 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/BC2288B5BA9B7BE352BA586257442E08 Binary files differnew file mode 100644 index 0000000000..a1d03856b5 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/BC2288B5BA9B7BE352BA586257442E08 diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/CFFC3831D8E7201BF8B77728FC79B52B b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/CFFC3831D8E7201BF8B77728FC79B52B Binary files differnew file mode 100644 index 0000000000..ba1145ca83 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/CFFC3831D8E7201BF8B77728FC79B52B diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/F3FA61DDA95B78A8B5F2C392C0382137 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/F3FA61DDA95B78A8B5F2C392C0382137 Binary files differnew file mode 100644 index 0000000000..82339b3b1d --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/F3FA61DDA95B78A8B5F2C392C0382137 diff --git a/browser/components/migration/tests/unit/Library/Safari/HistoryStrangeEntries.db b/browser/components/migration/tests/unit/Library/Safari/HistoryStrangeEntries.db Binary files differnew file mode 100644 index 0000000000..533daba3df --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/HistoryStrangeEntries.db diff --git a/browser/components/migration/tests/unit/Library/Safari/HistoryTemplate.db b/browser/components/migration/tests/unit/Library/Safari/HistoryTemplate.db Binary files differnew file mode 100644 index 0000000000..5a317c70e8 --- /dev/null +++ b/browser/components/migration/tests/unit/Library/Safari/HistoryTemplate.db 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 Binary files differnew file mode 100644 index 0000000000..b2d425eb4a --- /dev/null +++ b/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Default/Login Data 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/bookmarks.exported.html b/browser/components/migration/tests/unit/bookmarks.exported.html new file mode 100644 index 0000000000..5a9ec43325 --- /dev/null +++ b/browser/components/migration/tests/unit/bookmarks.exported.html @@ -0,0 +1,32 @@ +<!DOCTYPE NETSCAPE-Bookmark-file-1> +<!-- This is an automatically generated file. + It will be read and overwritten. + DO NOT EDIT! --> +<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> +<meta http-equiv="Content-Security-Policy" + content="default-src 'self'; script-src 'none'; img-src data: *; object-src 'none'"></meta> +<TITLE>Bookmarks</TITLE> +<H1>Bookmarks Menu</H1> + +<DL><p> + <DT><H3 ADD_DATE="1685118888" LAST_MODIFIED="1685118888">Mozilla Firefox</H3> + <DL><p> + <DT><A HREF="http://en-us.www.mozilla.com/en-US/firefox/help/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888" ICON_URI="fake-favicon-uri:http://en-us.www.mozilla.com/en-US/firefox/help/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==">Help and Tutorials</A> + <DT><A HREF="http://en-us.www.mozilla.com/en-US/firefox/customize/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888" ICON_URI="fake-favicon-uri:http://en-us.www.mozilla.com/en-US/firefox/customize/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==">Customize Firefox</A> + <DT><A HREF="http://en-us.www.mozilla.com/en-US/firefox/community/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888" ICON_URI="fake-favicon-uri:http://en-us.www.mozilla.com/en-US/firefox/community/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==">Get Involved</A> + <DT><A HREF="http://en-us.www.mozilla.com/en-US/about/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888" ICON_URI="fake-favicon-uri:http://en-us.www.mozilla.com/en-US/about/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==">About Us</A> + </DL><p> + <HR> <DT><H3 ADD_DATE="1177541020" LAST_MODIFIED="1177541050">test</H3> + <DL><p> + <DT><A HREF="http://test/post" ADD_DATE="1177375336" LAST_MODIFIED="1177375423" SHORTCUTURL="test" POST_DATA="hidden1%3Dbar&text1%3D%25s" LAST_CHARSET="ISO-8859-1">test post keyword</A> + </DL><p> + <DT><H3 ADD_DATE="1685118888" LAST_MODIFIED="1685118888" PERSONAL_TOOLBAR_FOLDER="true">Bookmarks Toolbar</H3> + <DL><p> + <DT><A HREF="http://en-us.www.mozilla.com/en-US/firefox/central/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888" ICON_URI="fake-favicon-uri:http://en-us.www.mozilla.com/en-US/firefox/central/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==">Getting Started</A> + <DT><A HREF="http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/" ADD_DATE="1177541035" LAST_MODIFIED="1177541035">Latest Headlines</A> + </DL><p> + <DT><H3 ADD_DATE="1685118888" LAST_MODIFIED="1685118888" UNFILED_BOOKMARKS_FOLDER="true">Other Bookmarks</H3> + <DL><p> + <DT><A HREF="http://example.tld/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888">Example.tld</A> + </DL><p> +</DL> diff --git a/browser/components/migration/tests/unit/bookmarks.exported.json b/browser/components/migration/tests/unit/bookmarks.exported.json new file mode 100644 index 0000000000..2a73f00b31 --- /dev/null +++ b/browser/components/migration/tests/unit/bookmarks.exported.json @@ -0,0 +1,194 @@ +{ + "guid": "root________", + "title": "", + "index": 0, + "dateAdded": 1685116351936000, + "lastModified": 1685372151518000, + "id": 1, + "typeCode": 2, + "type": "text/x-moz-place-container", + "root": "placesRoot", + "children": [ + { + "guid": "menu________", + "title": "menu", + "index": 0, + "dateAdded": 1685116351936000, + "lastModified": 1685116352325000, + "id": 2, + "typeCode": 2, + "type": "text/x-moz-place-container", + "root": "bookmarksMenuFolder", + "children": [ + { + "guid": "jCs_9YrgXKq7", + "title": "Firefox Nightly Resources", + "index": 0, + "dateAdded": 1685116352325000, + "lastModified": 1685116352325000, + "id": 7, + "typeCode": 2, + "type": "text/x-moz-place-container", + "children": [ + { + "guid": "xwdRLsUWYFwm", + "title": "Firefox Nightly blog", + "index": 0, + "dateAdded": 1685116352325000, + "lastModified": 1685116352325000, + "id": 8, + "typeCode": 1, + "iconUri": "fake-favicon-uri:https://blog.nightly.mozilla.org/", + "type": "text/x-moz-place", + "uri": "https://blog.nightly.mozilla.org/" + }, + { + "guid": "uhdiDrWjH0-n", + "title": "Mozilla Bug Tracker", + "index": 1, + "dateAdded": 1685116352325000, + "lastModified": 1685116352325000, + "id": 9, + "typeCode": 1, + "iconUri": "fake-favicon-uri:https://bugzilla.mozilla.org/", + "type": "text/x-moz-place", + "uri": "https://bugzilla.mozilla.org/", + "keyword": "bz", + "postData": null + }, + { + "guid": "zOK7d-gjJ5Vy", + "title": "Mozilla Developer Network", + "index": 2, + "dateAdded": 1685116352325000, + "lastModified": 1685116352325000, + "id": 10, + "typeCode": 1, + "iconUri": "fake-favicon-uri:https://developer.mozilla.org/", + "type": "text/x-moz-place", + "uri": "https://developer.mozilla.org/", + "keyword": "mdn", + "postData": null + }, + { + "guid": "7gcb4320A_y6", + "title": "Nightly Tester Tools", + "index": 3, + "dateAdded": 1685116352325000, + "lastModified": 1685116352325000, + "id": 11, + "typeCode": 1, + "iconUri": "fake-favicon-uri:https://addons.mozilla.org/firefox/addon/nightly-tester-tools/", + "type": "text/x-moz-place", + "uri": "https://addons.mozilla.org/firefox/addon/nightly-tester-tools/" + }, + { + "guid": "c4753lDvJwNE", + "title": "All your crashes", + "index": 4, + "dateAdded": 1685116352325000, + "lastModified": 1685116352325000, + "id": 12, + "typeCode": 1, + "iconUri": "fake-favicon-uri:about:crashes", + "type": "text/x-moz-place", + "uri": "about:crashes" + }, + { + "guid": "IyYGIH9VCs2t", + "title": "Planet Mozilla", + "index": 5, + "dateAdded": 1685116352325000, + "lastModified": 1685116352325000, + "id": 13, + "typeCode": 1, + "iconUri": "fake-favicon-uri:https://planet.mozilla.org/", + "type": "text/x-moz-place", + "uri": "https://planet.mozilla.org/" + } + ] + } + ] + }, + { + "guid": "toolbar_____", + "title": "toolbar", + "index": 1, + "dateAdded": 1685116351936000, + "lastModified": 1685372151518000, + "id": 3, + "typeCode": 2, + "type": "text/x-moz-place-container", + "root": "toolbarFolder", + "children": [ + { + "guid": "5jN1vdzOEnHx", + "title": "Get Involved", + "index": 0, + "dateAdded": 1685116352413000, + "lastModified": 1685116352413000, + "id": 14, + "typeCode": 1, + "iconUri": "fake-favicon-uri:https://www.mozilla.org/contribute/?utm_medium=firefox-desktop&utm_source=bookmarks-toolbar&utm_campaign=new-users-nightly&utm_content=-global", + "type": "text/x-moz-place", + "uri": "https://www.mozilla.org/contribute/?utm_medium=firefox-desktop&utm_source=bookmarks-toolbar&utm_campaign=new-users-nightly&utm_content=-global" + }, + { + "guid": "5RsMT9sWsmIe", + "title": "Why More Psychiatrists Think Mindfulness Can Help Treat ADHD", + "index": 1, + "dateAdded": 1685372143048000, + "lastModified": 1685372143048000, + "id": 15, + "typeCode": 1, + "type": "text/x-moz-place", + "uri": "https://getpocket.com/explore/item/why-more-psychiatrists-think-mindfulness-can-help-treat-adhd?utm_source=pocket-newtab" + }, + { + "guid": "ejoNUqAfEMQL", + "title": "Your New Favorite Weeknight Recipe Is Meat-Free (and Easy, Too)", + "index": 2, + "dateAdded": 1685372148200000, + "lastModified": 1685372148200000, + "id": 16, + "typeCode": 1, + "type": "text/x-moz-place", + "uri": "https://getpocket.com/collections/your-new-favorite-weeknight-recipe-is-meat-free-and-easy-too?utm_source=pocket-newtab" + }, + { + "guid": "O5QCiQ1zrqHY", + "title": "8 Natural Ways to Repel Insects Without Bug Spray", + "index": 3, + "dateAdded": 1685372151518000, + "lastModified": 1685372151518000, + "id": 17, + "typeCode": 1, + "type": "text/x-moz-place", + "uri": "https://getpocket.com/explore/item/8-natural-ways-to-repel-insects-without-bug-spray?utm_source=pocket-newtab" + } + ] + }, + { + "guid": "unfiled_____", + "title": "unfiled", + "index": 3, + "dateAdded": 1685116351936000, + "lastModified": 1685116352272000, + "id": 5, + "typeCode": 2, + "type": "text/x-moz-place-container", + "root": "unfiledBookmarksFolder" + }, + { + "guid": "mobile______", + "title": "mobile", + "index": 4, + "dateAdded": 1685116351968000, + "lastModified": 1685116352272000, + "id": 6, + "typeCode": 2, + "type": "text/x-moz-place-container", + "root": "mobileFolder" + } + ] +} diff --git a/browser/components/migration/tests/unit/bookmarks.invalid.html b/browser/components/migration/tests/unit/bookmarks.invalid.html new file mode 100644 index 0000000000..900ec52e1d --- /dev/null +++ b/browser/components/migration/tests/unit/bookmarks.invalid.html @@ -0,0 +1 @@ +This shouldn't cause anything to be imported. 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..9900f34232 --- /dev/null +++ b/browser/components/migration/tests/unit/head_migration.js @@ -0,0 +1,260 @@ +"use strict"; + +var { MigrationUtils } = ChromeUtils.importESModule( + "resource:///modules/MigrationUtils.sys.mjs" +); +var { LoginHelper } = ChromeUtils.importESModule( + "resource://gre/modules/LoginHelper.sys.mjs" +); +var { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); +var { PlacesUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PlacesUtils.sys.mjs" +); +var { Preferences } = ChromeUtils.importESModule( + "resource://gre/modules/Preferences.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" +); +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.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); +} +/** + * Function that returns a favicon url for a given page url + * + * @param {string} uri + * The Bookmark URI + * @returns {string} faviconURI + * The Favicon URI + */ +async function getFaviconForPageURI(uri) { + let faviconURI = await new Promise(resolve => { + PlacesUtils.favicons.getFaviconDataForPage(uri, favURI => { + resolve(favURI); + }); + }); + return faviconURI; +} + +/** + * Takes an array of page URIs and checks that the favicon was imported for each page URI + * + * @param {Array} pageURIs An array of page URIs + */ +async function assertFavicons(pageURIs) { + for (let uri of pageURIs) { + let faviconURI = await getFaviconForPageURI(uri); + Assert.ok(faviconURI, `Got favicon for ${uri.spec}`); + } +} + +/** + * 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); + } + }); +} + +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; +} + +/** + * Returns a PRTime value for the current date minus daysAgo number + * of days. + * + * @param {number} daysAgo + * How many days in the past from now the returned date should be. + * @returns {number} + */ +function PRTimeDaysAgo(daysAgo) { + return PlacesUtils.toPRTime(Date.now() - daysAgo * 24 * 60 * 60 * 1000); +} + +/** + * Returns a Date value for the current date minus daysAgo number + * of days. + * + * @param {number} daysAgo + * How many days in the past from now the returned date should be. + * @returns {Date} + */ +function dateDaysAgo(daysAgo) { + return new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000); +} + +/** + * Constructs and returns a data structure consistent with the Chrome + * browsers bookmarks storage. This data structure can then be serialized + * to JSON and written to disk to simulate a Chrome browser's bookmarks + * database. + * + * @param {number} [totalBookmarks=100] + * How many bookmarks to create. + * @returns {object} + */ +function createChromeBookmarkStructure(totalBookmarks = 100) { + let bookmarksData = { + roots: { + bookmark_bar: { children: [] }, + other: { children: [] }, + synced: { children: [] }, + }, + }; + const MAX_BMS = totalBookmarks; + let barKids = bookmarksData.roots.bookmark_bar.children; + let menuKids = bookmarksData.roots.other.children; + let syncedKids = bookmarksData.roots.synced.children; + let currentMenuKids = menuKids; + let currentBarKids = barKids; + let currentSyncedKids = syncedKids; + 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", + }); + currentSyncedKids.push({ + url: "https://www.chrome-synced-bookmark" + i + ".com", + name: "bookmark for synced " + 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; + + nextFolder = { + name: "synced folder " + Math.ceil(i / 20), + type: "folder", + children: [], + }; + currentSyncedKids.push(nextFolder); + currentSyncedKids = nextFolder.children; + } + } + return bookmarksData; +} 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..dea79a9289 --- /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..3a882b516d --- /dev/null +++ b/browser/components/migration/tests/unit/test_360seMigrationUtils.js @@ -0,0 +1,164 @@ +"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..e4f42d1880 --- /dev/null +++ b/browser/components/migration/tests/unit/test_360se_bookmarks.js @@ -0,0 +1,62 @@ +"use strict"; + +const { CustomizableUI } = ChromeUtils.importESModule( + "resource:///modules/CustomizableUI.sys.mjs" +); + +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_BookmarksFileMigrator.js b/browser/components/migration/tests/unit/test_BookmarksFileMigrator.js new file mode 100644 index 0000000000..ec10097e76 --- /dev/null +++ b/browser/components/migration/tests/unit/test_BookmarksFileMigrator.js @@ -0,0 +1,134 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { BookmarksFileMigrator } = ChromeUtils.importESModule( + "resource:///modules/FileMigrators.sys.mjs" +); + +const { MigrationWizardConstants } = ChromeUtils.importESModule( + "chrome://browser/content/migration/migration-wizard-constants.mjs" +); + +/** + * Tests that the BookmarksFileMigrator properly subclasses FileMigratorBase + * and delegates to BookmarkHTMLUtils or BookmarkJSONUtils. + * + * Normally, we'd override the BookmarkHTMLUtils and BookmarkJSONUtils methods + * in our test here so that we just ensure that they're called with the + * right arguments, rather than testing their behaviour. Unfortunately, both + * BookmarkHTMLUtils and BookmarkJSONUtils are frozen with Object.freeze, which + * prevents sinon from stubbing out any of their methods. Rather than unfreezing + * those objects just for testing, we test the whole flow end-to-end, including + * the import to Places. + */ + +add_setup(() => { + Services.prefs.setBoolPref("browser.migrate.bookmarks-file.enabled", true); + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); + Services.prefs.clearUserPref("browser.migrate.bookmarks-file.enabled"); + }); +}); + +/** + * First check that the BookmarksFileMigrator implements the required parts + * of the parent class. + */ +add_task(async function test_BookmarksFileMigrator_members() { + let migrator = new BookmarksFileMigrator(); + Assert.ok( + migrator.constructor.key, + `BookmarksFileMigrator implements static getter 'key'` + ); + + Assert.ok( + migrator.constructor.displayNameL10nID, + `BookmarksFileMigrator implements static getter 'displayNameL10nID'` + ); + + Assert.ok( + migrator.constructor.brandImage, + `BookmarksFileMigrator implements static getter 'brandImage'` + ); + + Assert.ok( + migrator.progressHeaderL10nID, + `BookmarksFileMigrator implements getter 'progressHeaderL10nID'` + ); + + Assert.ok( + migrator.successHeaderL10nID, + `BookmarksFileMigrator implements getter 'successHeaderL10nID'` + ); + + Assert.ok( + await migrator.getFilePickerConfig(), + `BookmarksFileMigrator implements method 'getFilePickerConfig'` + ); + + Assert.ok( + migrator.displayedResourceTypes, + `BookmarksFileMigrator implements getter 'displayedResourceTypes'` + ); + + Assert.ok(migrator.enabled, `BookmarksFileMigrator is enabled`); +}); + +add_task(async function test_BookmarksFileMigrator_HTML() { + let migrator = new BookmarksFileMigrator(); + const EXPECTED_SUCCESS_STATE = { + [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES + .BOOKMARKS_FROM_FILE]: "8 bookmarks", + }; + + const BOOKMARKS_PATH = PathUtils.join( + do_get_cwd().path, + "bookmarks.exported.html" + ); + + let result = await migrator.migrate(BOOKMARKS_PATH); + + Assert.deepEqual( + result, + EXPECTED_SUCCESS_STATE, + "Returned the expected success state." + ); +}); + +add_task(async function test_BookmarksFileMigrator_JSON() { + let migrator = new BookmarksFileMigrator(); + + const EXPECTED_SUCCESS_STATE = { + [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES + .BOOKMARKS_FROM_FILE]: "10 bookmarks", + }; + + const BOOKMARKS_PATH = PathUtils.join( + do_get_cwd().path, + "bookmarks.exported.json" + ); + + let result = await migrator.migrate(BOOKMARKS_PATH); + + Assert.deepEqual( + result, + EXPECTED_SUCCESS_STATE, + "Returned the expected success state." + ); +}); + +add_task(async function test_BookmarksFileMigrator_invalid() { + let migrator = new BookmarksFileMigrator(); + + const INVALID_FILE_PATH = PathUtils.join( + do_get_cwd().path, + "bookmarks.invalid.html" + ); + + await Assert.rejects( + migrator.migrate(INVALID_FILE_PATH), + /Pick another file/ + ); +}); 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..32a8d1b4bc --- /dev/null +++ b/browser/components/migration/tests/unit/test_ChromeMigrationUtils.js @@ -0,0 +1,87 @@ +"use strict"; + +const { ChromeMigrationUtils } = ChromeUtils.importESModule( + "resource:///modules/ChromeMigrationUtils.sys.mjs" +); + +// Setup chrome user data path for all platforms. +ChromeMigrationUtils.getDataPath = () => { + return Promise.resolve( + 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", + // There is no description in the `manifest.json` file of this extension. + description: null, + }, + "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..ca75595ea9 --- /dev/null +++ b/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js @@ -0,0 +1,141 @@ +"use strict"; + +const { ChromeMigrationUtils } = ChromeUtils.importESModule( + "resource:///modules/ChromeMigrationUtils.sys.mjs" +); + +const SUB_DIRECTORIES = { + win: { + Chrome: ["Google", "Chrome", "User Data"], + Chromium: ["Chromium", "User Data"], + Canary: ["Google", "Chrome SxS", "User Data"], + }, + macosx: { + Chrome: ["Application Support", "Google", "Chrome"], + Chromium: ["Application Support", "Chromium"], + Canary: ["Application Support", "Google", "Chrome Canary"], + }, + linux: { + Chrome: [".config", "google-chrome"], + Chromium: [".config", "chromium"], + Canary: [], + }, +}; + +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, do_get_file("chromefiles/", true)); +}); + +add_task(async function test_getDataPath_function() { + let projects = ["Chrome", "Chromium", "Canary"]; + let rootPath = getRootPath(); + + for (let project of projects) { + let subfolders = SUB_DIRECTORIES[AppConstants.platform][project]; + + await IOUtils.makeDirectory(PathUtils.join(rootPath, ...subfolders), { + createAncestor: true, + ignoreExisting: true, + }); + } + + let chromeUserDataPath = await ChromeMigrationUtils.getDataPath("Chrome"); + let chromiumUserDataPath = await ChromeMigrationUtils.getDataPath("Chromium"); + let canaryUserDataPath = await 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 = await 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_ChromeMigrationUtils_path_chromium_snap.js b/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path_chromium_snap.js new file mode 100644 index 0000000000..5011991536 --- /dev/null +++ b/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path_chromium_snap.js @@ -0,0 +1,55 @@ +/* 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 SUB_DIRECTORIES = { + linux: { + Chromium: [".config", "chromium"], + SnapChromium: ["snap", "chromium", "common", "chromium"], + }, +}; + +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, do_get_file("chromefiles/", true)); +}); + +add_task(async function test_getDataPath_function() { + let rootPath = getRootPath(); + let chromiumSubFolders = SUB_DIRECTORIES[AppConstants.platform].Chromium; + // must remove normal chromium path + await IOUtils.remove(PathUtils.join(rootPath, ...chromiumSubFolders), { + ignoreAbsent: true, + }); + + let snapChromiumSubFolders = + SUB_DIRECTORIES[AppConstants.platform].SnapChromium; + // must create snap chromium path + await IOUtils.makeDirectory( + PathUtils.join(rootPath, ...snapChromiumSubFolders), + { + createAncestor: true, + ignoreExisting: true, + } + ); + + let chromiumUserDataPath = await ChromeMigrationUtils.getDataPath("Chromium"); + Assert.equal( + chromiumUserDataPath, + PathUtils.join(getRootPath(), ...snapChromiumSubFolders), + "Should get the path of Snap Chromium data 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..d115cda412 --- /dev/null +++ b/browser/components/migration/tests/unit/test_Chrome_bookmarks.js @@ -0,0 +1,205 @@ +"use strict"; + +const { CustomizableUI } = ChromeUtils.importESModule( + "resource:///modules/CustomizableUI.sys.mjs" +); + +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 + while (subDirs.length) { + target.append(subDirs.shift()); + } + + let localStatePath = PathUtils.join(target.path, "Local State"); + await IOUtils.writeJSON(localStatePath, []); + + target.append("Default"); + + await IOUtils.makeDirectory(target.path, { + createAncestor: true, + ignoreExisting: true, + }); + + // Copy Favicons database into Default profile + const sourcePath = do_get_file( + "AppData/Local/Google/Chrome/User Data/Default/Favicons" + ).path; + await IOUtils.copy(sourcePath, target.path); + + // Get page url for each favicon + let faviconURIs = await MigrationUtils.getRowsFromDBWithoutLocks( + sourcePath, + "Chrome Bookmark Favicons", + `select page_url from icon_mapping` + ); + + target.append("Bookmarks"); + await IOUtils.remove(target.path, { ignoreAbsent: true }); + + let bookmarksData = createChromeBookmarkStructure(); + await IOUtils.writeJSON(target.path, bookmarksData); + + let migrator = await MigrationUtils.getMigrator(migratorKey); + Assert.ok(await migrator.hasPermissions(), "Has permissions"); + // 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, + 210, + "Should have seen 210 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, 300, "Should have seen 300 bookmarks."); + Assert.equal(itemsSeen.folders, 15, "Should have seen 15 folders."); + Assert.equal( + MigrationUtils._importQuantities.bookmarks, + itemsSeen.bookmarks + itemsSeen.folders, + "Telemetry reporting correct." + ); + Assert.ok(observerNotified, "The observer should be notified upon migration"); + let pageUrls = Array.from(faviconURIs, f => + Services.io.newURI(f.getResultByName("page_url")) + ); + await assertFavicons(pageUrls); +} + +add_task(async function test_Chrome() { + // Expire all favicons before the test to make sure favicons are imported + PlacesUtils.favicons.expireAllFavicons(); + let subDirs = + AppConstants.platform == "linux" ? ["google-chrome"] : ["Google", "Chrome"]; + await testBookmarks("chrome", subDirs); +}); + +add_task(async function test_ChromiumEdge() { + PlacesUtils.favicons.expireAllFavicons(); + 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_corrupt_history.js b/browser/components/migration/tests/unit/test_Chrome_corrupt_history.js new file mode 100644 index 0000000000..31541f9fdb --- /dev/null +++ b/browser/components/migration/tests/unit/test_Chrome_corrupt_history.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let rootDir = do_get_file("chromefiles/", true); + +const SOURCE_PROFILE_DIR = "Library/Application Support/Google/Chrome/Default/"; +const PROFILE = { + id: "Default", + name: "Person 1", +}; + +add_setup(async function setup_fake_paths() { + let pathId; + if (AppConstants.platform == "macosx") { + pathId = "ULibDir"; + } else if (AppConstants.platform == "win") { + pathId = "LocalAppData"; + } else { + pathId = "Home"; + } + registerFakePath(pathId, rootDir); + + let file = do_get_file(`${SOURCE_PROFILE_DIR}HistoryCorrupt`); + file.copyTo(file.parent, "History"); + + registerCleanupFunction(() => { + let historyFile = do_get_file(`${SOURCE_PROFILE_DIR}History`, true); + try { + historyFile.remove(false); + } catch (ex) { + // It is ok if this doesn't exist. + if (ex.result != Cr.NS_ERROR_FILE_NOT_FOUND) { + throw ex; + } + } + }); + + let subDirs = + AppConstants.platform == "linux" ? ["google-chrome"] : ["Google", "Chrome"]; + + 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 = createChromeBookmarkStructure(); + await IOUtils.writeJSON(target.path, bookmarksData); +}); + +add_task(async function test_corrupt_history() { + let migrator = await MigrationUtils.getMigrator("chrome"); + Assert.ok(await migrator.isSourceAvailable()); + + let data = await migrator.getMigrateData(PROFILE); + Assert.ok( + data & MigrationUtils.resourceTypes.BOOKMARKS, + "Bookmarks resource available." + ); + Assert.ok( + !(data & MigrationUtils.resourceTypes.HISTORY), + "Corrupt history resource unavailable." + ); +}); diff --git a/browser/components/migration/tests/unit/test_Chrome_credit_cards.js b/browser/components/migration/tests/unit/test_Chrome_credit_cards.js new file mode 100644 index 0000000000..5c4d3517d2 --- /dev/null +++ b/browser/components/migration/tests/unit/test_Chrome_credit_cards.js @@ -0,0 +1,239 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global structuredClone */ + +const PROFILE = { + id: "Default", + name: "Default", +}; + +const PAYMENT_METHODS = [ + { + name_on_card: "Name Name", + card_number: "4532962432748929", // Visa + expiration_month: 3, + expiration_year: 2027, + }, + { + name_on_card: "Name Name Name", + card_number: "5359908373796416", // Mastercard + expiration_month: 5, + expiration_year: 2028, + }, + { + name_on_card: "Name", + card_number: "346624461807588", // AMEX + expiration_month: 4, + expiration_year: 2026, + }, +]; + +let OSKeyStoreTestUtils; +add_setup(async function os_key_store_setup() { + ({ OSKeyStoreTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/OSKeyStoreTestUtils.sys.mjs" + )); + OSKeyStoreTestUtils.setup(); + registerCleanupFunction(async function cleanup() { + await OSKeyStoreTestUtils.cleanup(); + }); +}); + +let rootDir = do_get_file("chromefiles/", true); + +function checkCardsAreEqual(importedCard, testCard, id) { + const CC_NUMBER_RE = /^(\*+)(.{4})$/; + + Assert.equal( + importedCard["cc-name"], + testCard.name_on_card, + "The two logins ID " + id + " have the same name on card" + ); + + let matches = CC_NUMBER_RE.exec(importedCard["cc-number"]); + Assert.notEqual(matches, null); + Assert.equal(importedCard["cc-number"].length, testCard.card_number.length); + Assert.equal(testCard.card_number.endsWith(matches[2]), true); + Assert.notEqual(importedCard["cc-number-encrypted"], ""); + + Assert.equal( + importedCard["cc-exp-month"], + testCard.expiration_month, + "The two logins ID " + id + " have the same expiration month" + ); + Assert.equal( + importedCard["cc-exp-year"], + testCard.expiration_year, + "The two logins ID " + id + " have the same expiration year" + ); +} + +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 test_credit_cards() { + if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) { + todo_check_true( + OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), + "Cannot test OS key store login on official builds." + ); + return; + } + + let loginCrypto; + let profilePathSegments; + + 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 + ); + profilePathSegments = [ + "Application Support", + "Google", + "Chrome", + "Default", + ]; + } else if (AppConstants.platform == "win") { + let { ChromeWindowsLoginCrypto } = ChromeUtils.importESModule( + "resource:///modules/ChromeWindowsLoginCrypto.sys.mjs" + ); + loginCrypto = new ChromeWindowsLoginCrypto("Chrome"); + profilePathSegments = ["Google", "Chrome", "User Data", "Default"]; + } else { + throw new Error("Not implemented"); + } + + let target = rootDir.clone(); + let defaultFolderPath = PathUtils.join(target.path, ...profilePathSegments); + let webDataPath = PathUtils.join(defaultFolderPath, "Web Data"); + let localStatePath = defaultFolderPath.replace("Default", ""); + + await IOUtils.makeDirectory(defaultFolderPath, { + createAncestor: true, + ignoreExisting: true, + }); + + // Copy Web Data database into Default profile + const sourcePathWebData = do_get_file( + "AppData/Local/Google/Chrome/User Data/Default/Web Data" + ).path; + await IOUtils.copy(sourcePathWebData, webDataPath); + + const sourcePathLocalState = do_get_file( + "AppData/Local/Google/Chrome/User Data/Local State" + ).path; + await IOUtils.copy(sourcePathLocalState, localStatePath); + + let dbConn = await Sqlite.openConnection({ path: webDataPath }); + + for (let card of PAYMENT_METHODS) { + let encryptedCardNumber = await loginCrypto.encryptData(card.card_number); + let cardNumberEncryptedValue = new Uint8Array( + loginCrypto.stringToArray(encryptedCardNumber) + ); + + let cardCopy = structuredClone(card); + + cardCopy.card_number_encrypted = cardNumberEncryptedValue; + delete cardCopy.card_number; + + await dbConn.execute( + `INSERT INTO credit_cards + (name_on_card, card_number_encrypted, expiration_month, expiration_year) + VALUES (:name_on_card, :card_number_encrypted, :expiration_month, :expiration_year) + `, + cardCopy + ); + } + + await dbConn.close(); + + let migrator = await MigrationUtils.getMigrator("chrome"); + if (AppConstants.platform == "macosx") { + Object.assign(migrator, { + _keychainServiceName: mockMacOSKeychain.serviceName, + _keychainAccountName: mockMacOSKeychain.accountName, + _keychainMockPassphrase: mockMacOSKeychain.passphrase, + }); + } + Assert.ok( + await migrator.isSourceAvailable(), + "Sanity check the source exists" + ); + + Services.prefs.setBoolPref( + "browser.migrate.chrome.payment_methods.enabled", + false + ); + Assert.ok( + !( + (await migrator.getMigrateData(PROFILE)) & + MigrationUtils.resourceTypes.PAYMENT_METHODS + ), + "Should be able to disable migrating payment methods" + ); + // Clear the cached resources now so that a re-check for payment methods + // will look again. + delete migrator._resourcesByProfile[PROFILE.id]; + + Services.prefs.setBoolPref( + "browser.migrate.chrome.payment_methods.enabled", + true + ); + + Assert.ok( + (await migrator.getMigrateData(PROFILE)) & + MigrationUtils.resourceTypes.PAYMENT_METHODS, + "Should be able to enable migrating payment methods" + ); + + let { formAutofillStorage } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillStorage.sys.mjs" + ); + await formAutofillStorage.initialize(); + + await promiseMigration( + migrator, + MigrationUtils.resourceTypes.PAYMENT_METHODS, + PROFILE + ); + + let cards = await formAutofillStorage.creditCards.getAll(); + + Assert.equal( + cards.length, + PAYMENT_METHODS.length, + "Check there are still the same number of credit cards after re-importing the data" + ); + Assert.equal( + cards.length, + MigrationUtils._importQuantities.cards, + "Check telemetry matches the actual import." + ); + + for (let i = 0; i < PAYMENT_METHODS.length; i++) { + checkCardsAreEqual(cards[i], PAYMENT_METHODS[i], i + 1); + } +}); diff --git a/browser/components/migration/tests/unit/test_Chrome_extensions.js b/browser/components/migration/tests/unit/test_Chrome_extensions.js new file mode 100644 index 0000000000..7aa7a94194 --- /dev/null +++ b/browser/components/migration/tests/unit/test_Chrome_extensions.js @@ -0,0 +1,165 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AMBrowserExtensionsImport, AddonManager } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { ChromeMigrationUtils } = ChromeUtils.importESModule( + "resource:///modules/ChromeMigrationUtils.sys.mjs" +); +const { ChromeProfileMigrator } = ChromeUtils.importESModule( + "resource:///modules/ChromeProfileMigrator.sys.mjs" +); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +const TEST_SERVER = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); + +const PROFILE = { + id: "Default", + name: "Person 1", +}; + +const IMPORTED_ADDON_1 = { + name: "A Firefox extension", + version: "1.0", + browser_specific_settings: { gecko: { id: "some-ff@extension" } }, +}; + +// Setup chrome user data path for all platforms. +ChromeMigrationUtils.getDataPath = () => { + return Promise.resolve( + do_get_file("Library/Application Support/Google/Chrome/").path + ); +}; + +const mockAddonRepository = ({ addons = [] } = {}) => { + return { + async getMappedAddons(browserID, extensionIDs) { + Assert.equal(browserID, "chrome", "expected browser ID"); + // Sort extension IDs to have a predictable order. + extensionIDs.sort(); + Assert.deepEqual( + extensionIDs, + ["fake-extension-1", "fake-extension-2"], + "expected an array of 2 extension IDs" + ); + return Promise.resolve({ + addons, + matchedIDs: [], + unmatchedIDs: [], + }); + }, + }; +}; + +add_setup(async function setup() { + await AddonTestUtils.promiseStartupManager(); + + // Create a Firefox XPI that we can use during the import. + const xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: IMPORTED_ADDON_1, + }); + TEST_SERVER.registerFile(`/addons/addon-1.xpi`, xpi); + + // Override the `AddonRepository` in `AMBrowserExtensionsImport` with our own + // mock so that we control the add-ons that are mapped to the list of Chrome + // extension IDs. + const addons = [ + { + id: IMPORTED_ADDON_1.browser_specific_settings.gecko.id, + name: IMPORTED_ADDON_1.name, + version: IMPORTED_ADDON_1.version, + sourceURI: Services.io.newURI(`http://example.com/addons/addon-1.xpi`), + icons: {}, + }, + ]; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ addons }); + + registerCleanupFunction(() => { + // Clear the add-on repository override. + AMBrowserExtensionsImport._addonRepository = null; + }); +}); + +add_task( + { pref_set: [["browser.migrate.chrome.extensions.enabled", true]] }, + async function test_import_extensions() { + const migrator = await MigrationUtils.getMigrator("chrome"); + Assert.ok( + await migrator.isSourceAvailable(), + "Sanity check the source exists" + ); + + let promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + await promiseMigration( + migrator, + MigrationUtils.resourceTypes.EXTENSIONS, + PROFILE, + true + ); + await promiseTopic; + // When this property is `true`, the UI should show a badge on the appmenu + // button, and the user can finalize the import later. + Assert.ok( + AMBrowserExtensionsImport.canCompleteOrCancelInstalls, + "expected some add-ons to have been imported" + ); + + // Let's actually complete the import programatically below. + promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-complete" + ); + await AMBrowserExtensionsImport.completeInstalls(); + await Promise.all([ + AddonTestUtils.promiseInstallEvent("onInstallEnded"), + promiseTopic, + ]); + + // The add-on should be installed and therefore it can be uninstalled. + const addon = await AddonManager.getAddonByID( + IMPORTED_ADDON_1.browser_specific_settings.gecko.id + ); + await addon.uninstall(); + } +); + +/** + * Test that, for now at least, the extension resource type is only made + * available for Chrome and none of the derivitive browsers. + */ +add_task( + { pref_set: [["browser.migrate.chrome.extensions.enabled", true]] }, + async function test_only_chrome_migrates_extensions() { + for (const key of MigrationUtils.availableMigratorKeys) { + let migrator = await MigrationUtils.getMigrator(key); + + if (migrator instanceof ChromeProfileMigrator && key != "chrome") { + info("Testing migrator with key " + key); + Assert.ok( + await migrator.isSourceAvailable(), + "First check the source exists" + ); + let resourceTypes = await migrator.getMigrateData(PROFILE); + Assert.ok( + !(resourceTypes & MigrationUtils.resourceTypes.EXTENSIONS), + "Should not offer the extension resource type" + ); + } + } + } +); diff --git a/browser/components/migration/tests/unit/test_Chrome_formdata.js b/browser/components/migration/tests/unit/test_Chrome_formdata.js new file mode 100644 index 0000000000..1dc411cb14 --- /dev/null +++ b/browser/components/migration/tests/unit/test_Chrome_formdata.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { FormHistory } = ChromeUtils.importESModule( + "resource://gre/modules/FormHistory.sys.mjs" +); + +let rootDir = do_get_file("chromefiles/", true); + +add_setup(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); +}); + +/** + * This function creates a testing database in the default profile, + * populates it with 10 example data entries,migrates the database, + * and then searches for each entry to ensure it exists in the FormHistory. + * + * @async + * @param {string} migratorKey + * A string that identifies the type of migrator object to be retrieved. + * @param {Array<string>} subDirs + * An array of strings that specifies the subdirectories for the target profile directory. + * @returns {Promise<undefined>} + * A Promise that resolves when the migration is completed. + */ +async function testFormdata(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("Web Data"); + await IOUtils.remove(target.path, { ignoreAbsent: true }); + + // Clear any search history results + await FormHistory.update({ op: "remove" }); + + let dbConn = await Sqlite.openConnection({ path: target.path }); + + await dbConn.execute( + `CREATE TABLE "autofill" (name VARCHAR, value VARCHAR, value_lower VARCHAR, date_created INTEGER DEFAULT 0, date_last_used INTEGER DEFAULT 0, count INTEGER DEFAULT 1, PRIMARY KEY (name, value))` + ); + for (let i = 0; i < 10; i++) { + await dbConn.execute( + `INSERT INTO autofill VALUES (:name, :value, :value_lower, :date_created, :date_last_used, :count)`, + { + name: `name${i}`, + value: `example${i}`, + value_lower: `example${i}`, + date_created: Math.round(Date.now() / 1000) - i * 10000, + date_last_used: Date.now(), + count: i, + } + ); + } + await dbConn.close(); + + let migrator = await MigrationUtils.getMigrator(migratorKey); + // Sanity check for the source. + Assert.ok(await migrator.isSourceAvailable()); + + await promiseMigration(migrator, MigrationUtils.resourceTypes.FORMDATA, { + id: "Default", + name: "Person 1", + }); + + for (let i = 0; i < 10; i++) { + let results = await FormHistory.search(["fieldname", "value"], { + fieldname: `name${i}`, + value: `example${i}`, + }); + Assert.ok(results.length, `Should have item${i} in FormHistory`); + } +} + +add_task(async function test_Chrome() { + let subDirs = + AppConstants.platform == "linux" ? ["google-chrome"] : ["Google", "Chrome"]; + await testFormdata("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 testFormdata("chromium-edge", subDirs); +}); 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..374b697c75 --- /dev/null +++ b/browser/components/migration/tests/unit/test_Chrome_passwords.js @@ -0,0 +1,373 @@ +"use strict"; + +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) { + const encryptedString = await loginCrypto.encryptData( + login.password, + login.version + ); + info(`promiseSetPassword: ${encryptedString}`); + const 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) { + const 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. + const mockMacOSKeychain = { + passphrase: "bW96aWxsYWZpcmVmb3g=", + serviceName: "TESTING Chrome Safe Storage", + accountName: "TESTING Chrome", + }; + if (AppConstants.platform == "macosx") { + const { 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") { + const { 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"); + } + const dirSvcFile = do_get_file(dirSvcPath); + registerFakePath(pathId, dirSvcFile); + + info(PathUtils.join(dirSvcFile.path, ...profilePathSegments)); + const loginDataFilePath = PathUtils.join( + dirSvcFile.path, + ...profilePathSegments + ); + dbConn = await Sqlite.openConnection({ path: loginDataFilePath }); + + if (AppConstants.platform == "macosx") { + const 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 (const login of TEST_LOGINS) { + await promiseSetPassword(login); + } + + const migrator = await MigrationUtils.getMigrator("chrome"); + Assert.ok( + await migrator.isSourceAvailable(), + "Sanity check the source exists" + ); + + let logins = await 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 = await 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() { + const migrator = await MigrationUtils.getMigrator("chrome"); + Assert.ok( + await migrator.isSourceAvailable(), + "Sanity check the source exists" + ); + + Services.logins.removeAllUserFacingLogins(); + let logins = await Services.logins.getAllLogins(); + Assert.equal( + logins.length, + 0, + "There are no logins after removing all of them" + ); + + const 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])); + await Services.logins.addLoginAsync(newLogins[i]); + } + + logins = await 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 = await 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..0e6993fded --- /dev/null +++ b/browser/components/migration/tests/unit/test_Chrome_passwords_emptySource.js @@ -0,0 +1,43 @@ +"use strict"; + +const { ChromeProfileMigrator } = ChromeUtils.importESModule( + "resource:///modules/ChromeProfileMigrator.sys.mjs" +); + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +add_task(async function test_importEmptyDBWithoutAuthPrompts() { + let dirSvcPath; + let pathId; + + 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); + + let sandbox = sinon.createSandbox(); + sandbox + .stub(ChromeProfileMigrator.prototype, "canGetPermissions") + .resolves(true); + sandbox + .stub(ChromeProfileMigrator.prototype, "hasPermissions") + .resolves(true); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + let migrator = await MigrationUtils.getMigrator("chrome"); + Assert.ok( + !migrator, + "Migrator should not be available since there are no passwords" + ); +}); diff --git a/browser/components/migration/tests/unit/test_Chrome_permissions.js b/browser/components/migration/tests/unit/test_Chrome_permissions.js new file mode 100644 index 0000000000..6dfd8bcceb --- /dev/null +++ b/browser/components/migration/tests/unit/test_Chrome_permissions.js @@ -0,0 +1,205 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if the migrator does not have the permission to + * read from the Chrome data directory, that it can request + * permission to read from it, if the system allows. + */ + +const { ChromeMigrationUtils } = ChromeUtils.importESModule( + "resource:///modules/ChromeMigrationUtils.sys.mjs" +); + +const { ChromeProfileMigrator } = ChromeUtils.importESModule( + "resource:///modules/ChromeProfileMigrator.sys.mjs" +); + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +const { MockFilePicker } = ChromeUtils.importESModule( + "resource://testing-common/MockFilePicker.sys.mjs" +); + +let gTempDir; + +add_setup(async () => { + Services.prefs.setBoolPref( + "browser.migrate.chrome.get_permissions.enabled", + true + ); + gTempDir = do_get_tempdir(); + await IOUtils.writeJSON(PathUtils.join(gTempDir.path, "Local State"), []); + + MockFilePicker.init(globalThis); + registerCleanupFunction(() => { + MockFilePicker.cleanup(); + }); +}); + +/** + * Tests that canGetPermissions will return false if the platform does + * not allow for folder selection in the native file picker, and returns + * the data path otherwise. + */ +add_task(async function test_canGetPermissions() { + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + let migrator = new ChromeProfileMigrator(); + let canGetPermissionsStub = sandbox + .stub(MigrationUtils, "canGetPermissionsOnPlatform") + .resolves(false); + sandbox.stub(ChromeMigrationUtils, "getDataPath").resolves(gTempDir.path); + + Assert.ok( + !(await migrator.canGetPermissions()), + "Should not be able to get permissions." + ); + + canGetPermissionsStub.resolves(true); + + Assert.equal( + await migrator.canGetPermissions(), + gTempDir.path, + "Should be able to get the permissions path." + ); + + sandbox.restore(); +}); + +/** + * Tests that getPermissions will show the native file picker in a + * loop until either the user cancels or selects a folder that grants + * read permissions to the data directory. + */ +add_task(async function test_getPermissions() { + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + let migrator = new ChromeProfileMigrator(); + sandbox.stub(MigrationUtils, "canGetPermissionsOnPlatform").resolves(true); + sandbox.stub(ChromeMigrationUtils, "getDataPath").resolves(gTempDir.path); + let hasPermissionsStub = sandbox + .stub(migrator, "hasPermissions") + .resolves(false); + + Assert.equal( + await migrator.canGetPermissions(), + gTempDir.path, + "Should be able to get the permissions path." + ); + + let filePickerSeenCount = 0; + + let filePickerShownPromise = new Promise(resolve => { + MockFilePicker.showCallback = () => { + Assert.ok(true, "Filepicker shown."); + MockFilePicker.useDirectory([gTempDir.path]); + filePickerSeenCount++; + if (filePickerSeenCount > 3) { + Assert.ok(true, "File picker looped 3 times."); + hasPermissionsStub.resolves(true); + resolve(); + } + }; + }); + MockFilePicker.returnValue = MockFilePicker.returnOK; + Assert.ok( + await migrator.getPermissions(), + "Should report that we got permissions." + ); + + await filePickerShownPromise; + + // Make sure that the user can also hit "cancel" and that we + // file picker loop. + + hasPermissionsStub.resolves(false); + + filePickerSeenCount = 0; + filePickerShownPromise = new Promise(resolve => { + MockFilePicker.showCallback = () => { + Assert.ok(true, "Filepicker shown."); + filePickerSeenCount++; + Assert.equal(filePickerSeenCount, 1, "Saw the picker once."); + resolve(); + }; + }); + MockFilePicker.returnValue = MockFilePicker.returnCancel; + Assert.ok( + !(await migrator.getPermissions()), + "Should report that we didn't get permissions." + ); + await filePickerShownPromise; + + sandbox.restore(); +}); + +/** + * Tests that if the native file picker chooses a different directory + * than the one we originally asked for, that we remap attempts to + * read profiles from that new directory. This is because Ubuntu Snaps + * will return us paths from the native file picker that are symlinks + * to the original directories. + */ +add_task(async function test_remapDirectories() { + let remapDir = new FileUtils.File( + await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "test-chrome-migration" + ) + ); + let localStatePath = PathUtils.join(remapDir.path, "Local State"); + await IOUtils.writeJSON(localStatePath, []); + + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + let migrator = new ChromeProfileMigrator(); + sandbox.stub(MigrationUtils, "canGetPermissionsOnPlatform").resolves(true); + sandbox.stub(ChromeMigrationUtils, "getDataPath").resolves(gTempDir.path); + let hasPermissionsStub = sandbox + .stub(migrator, "hasPermissions") + .resolves(false); + + Assert.equal( + await migrator.canGetPermissions(), + gTempDir.path, + "Should be able to get the permissions path." + ); + + let filePickerShownPromise = new Promise(resolve => { + MockFilePicker.showCallback = () => { + Assert.ok(true, "Filepicker shown."); + MockFilePicker.useDirectory([remapDir.path]); + hasPermissionsStub.resolves(true); + resolve(); + }; + }); + MockFilePicker.returnValue = MockFilePicker.returnOK; + Assert.ok( + await migrator.getPermissions(), + "Should report that we got permissions." + ); + + Assert.equal( + PathUtils.normalize(await migrator.canGetPermissions()), + PathUtils.normalize(remapDir.path), + "Should be able to get the remapped permissions path." + ); + + await filePickerShownPromise; + + sandbox.restore(); +}); 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..3b2672d9d8 --- /dev/null +++ b/browser/components/migration/tests/unit/test_Edge_db_migration.js @@ -0,0 +1,849 @@ +"use strict"; + +const { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" +); +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) { + console.error(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, + }, + ]; + + // The following entries are expected to be skipped as being too old to + // migrate. + let expiredTypedURLsReferenceItems = [ + { + URL: "https://expired1.invalid/", + AccessDateTimeUTC: dateDaysAgo(500), + }, + { + URL: "https://expired2.invalid/", + AccessDateTimeUTC: dateDaysAgo(300), + }, + { + URL: "https://expired3.invalid/", + AccessDateTimeUTC: dateDaysAgo(190), + }, + ]; + + // The following entries should be new enough to migrate. + let unexpiredTypedURLsReferenceItems = [ + { + URL: "https://unexpired1.invalid/", + AccessDateTimeUTC: dateDaysAgo(179), + }, + { + URL: "https://unexpired2.invalid/", + AccessDateTimeUTC: dateDaysAgo(50), + }, + { + URL: "https://unexpired3.invalid/", + }, + ]; + + let typedURLsReferenceItems = [ + ...expiredTypedURLsReferenceItems, + ...unexpiredTypedURLsReferenceItems, + ]; + + Assert.ok( + MigrationUtils.HISTORY_MAX_AGE_IN_DAYS < 300, + "This test expects the current pref to be less than the youngest expired visit." + ); + Assert.ok( + MigrationUtils.HISTORY_MAX_AGE_IN_DAYS > 160, + "This test expects the current pref to be greater than the oldest unexpired visit." + ); + + 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, + }, + ], + [ + "TypedURLs", + { + columns: [ + { type: COLUMN_TYPES.JET_coltypLongText, name: "URL", cbMax: 4096 }, + { + type: COLUMN_TYPES.JET_coltypLongLong, + name: "AccessDateTimeUTC", + }, + ], + rows: typedURLsReferenceItems, + }, + ], + ]) + ); + + // 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 => { + console.error(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 => { + console.error(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( + "migration-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." + ); + + let historyDBMigrator = migrator.getHistoryDBMigratorForTesting(db); + await new Promise(resolve => { + historyDBMigrator.migrate(resolve); + }); + Assert.ok(true, "History DB migration done!"); + for (let expiredEntry of expiredTypedURLsReferenceItems) { + let entry = await PlacesUtils.history.fetch(expiredEntry.URL, { + includeVisits: true, + }); + Assert.equal(entry, null, "Should not have found an entry."); + } + + for (let unexpiredEntry of unexpiredTypedURLsReferenceItems) { + let entry = await PlacesUtils.history.fetch(unexpiredEntry.URL, { + includeVisits: true, + }); + Assert.equal(entry.url, unexpiredEntry.URL, "Should have the correct URL"); + Assert.ok(!!entry.visits.length, "Should have some visits"); + } +}); diff --git a/browser/components/migration/tests/unit/test_Edge_registry_migration.js b/browser/components/migration/tests/unit/test_Edge_registry_migration.js new file mode 100644 index 0000000000..2a400f7858 --- /dev/null +++ b/browser/components/migration/tests/unit/test_Edge_registry_migration.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); +const { EdgeProfileMigrator } = ChromeUtils.importESModule( + "resource:///modules/EdgeProfileMigrator.sys.mjs" +); +const { MSMigrationUtils } = ChromeUtils.importESModule( + "resource:///modules/MSMigrationUtils.sys.mjs" +); + +/** + * Tests that history visits loaded from the registry from Edge (EdgeHTML) + * that have a visit date older than maxAgeInDays days do not get imported. + */ +add_task(async function test_Edge_history_past_max_days() { + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + Assert.ok( + MigrationUtils.HISTORY_MAX_AGE_IN_DAYS < 300, + "This test expects the current pref to be less than the youngest expired visit." + ); + Assert.ok( + MigrationUtils.HISTORY_MAX_AGE_IN_DAYS > 160, + "This test expects the current pref to be greater than the oldest unexpired visit." + ); + + const EXPIRED_VISITS = [ + ["https://test1.invalid/", dateDaysAgo(500).getTime() * 1000], + ["https://test2.invalid/", dateDaysAgo(450).getTime() * 1000], + ["https://test3.invalid/", dateDaysAgo(300).getTime() * 1000], + ]; + + const UNEXPIRED_VISITS = [ + ["https://test4.invalid/"], + ["https://test5.invalid/", dateDaysAgo(160).getTime() * 1000], + ["https://test6.invalid/", dateDaysAgo(50).getTime() * 1000], + ["https://test7.invalid/", dateDaysAgo(0).getTime() * 1000], + ]; + + const ALL_VISITS = [...EXPIRED_VISITS, ...UNEXPIRED_VISITS]; + + // Fake out the getResources method of the migrator so that we return + // a single fake MigratorResource per availableResourceType. + sandbox.stub(MSMigrationUtils, "getTypedURLs").callsFake(() => { + return new Map(ALL_VISITS); + }); + + // 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 registryTypedHistoryMigrator = + migrator.getHistoryRegistryMigratorForTesting(); + await new Promise(resolve => { + registryTypedHistoryMigrator.migrate(resolve); + }); + Assert.ok(true, "History from registry migration done!"); + + for (let expiredEntry of EXPIRED_VISITS) { + let entry = await PlacesUtils.history.fetch(expiredEntry[0], { + includeVisits: true, + }); + Assert.equal(entry, null, "Should not have found an entry."); + } + + for (let unexpiredEntry of UNEXPIRED_VISITS) { + let entry = await PlacesUtils.history.fetch(unexpiredEntry[0], { + includeVisits: true, + }); + Assert.equal(entry.url, unexpiredEntry[0], "Should have the correct URL"); + Assert.ok(!!entry.visits.length, "Should have some visits"); + } +}); 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..9816bb16e3 --- /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_history.js b/browser/components/migration/tests/unit/test_IE_history.js new file mode 100644 index 0000000000..f9a1e719a2 --- /dev/null +++ b/browser/components/migration/tests/unit/test_IE_history.js @@ -0,0 +1,187 @@ +"use strict"; + +const { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); + +// 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"); + } + + await PlacesUtils.history.clear(); +}); + +/** + * Tests that history visits from IE that have a visit date older than + * maxAgeInDays days do not get imported. + */ +add_task(async function test_IE_history_past_max_days() { + // The InsertIEHistory program inserts two history visits using the MS COM + // IUrlHistoryStg interface. That interface does not allow us to dictate + // the visit times of those history visits. Thankfully, we can temporarily + // mock out the @mozilla.org/profile/migrator/iehistoryenumerator;1 to return + // some entries that we expect to expire. + + /** + * An implmentation of nsISimpleEnumerator that wraps a JavaScript Array. + */ + class nsSimpleEnumerator { + #items; + #nextIndex; + + constructor(items) { + this.#items = items; + this.#nextIndex = 0; + } + + hasMoreElements() { + return this.#nextIndex < this.#items.length; + } + + getNext() { + if (!this.hasMoreElements()) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + + return this.#items[this.#nextIndex++]; + } + + [Symbol.iterator]() { + return this.#items.values(); + } + + QueryInterface = ChromeUtils.generateQI(["nsISimpleEnumerator"]); + } + + Assert.ok( + MigrationUtils.HISTORY_MAX_AGE_IN_DAYS < 300, + "This test expects the current pref to be less than the youngest expired visit." + ); + Assert.ok( + MigrationUtils.HISTORY_MAX_AGE_IN_DAYS > 160, + "This test expects the current pref to be greater than the oldest unexpired visit." + ); + + const EXPIRED_VISITS = [ + new Map([ + ["uri", Services.io.newURI("https://test1.invalid")], + ["title", "Test history visit 1"], + ["time", PRTimeDaysAgo(500)], + ]), + new Map([ + ["uri", Services.io.newURI("https://test2.invalid")], + ["title", "Test history visit 2"], + ["time", PRTimeDaysAgo(450)], + ]), + new Map([ + ["uri", Services.io.newURI("https://test3.invalid")], + ["title", "Test history visit 3"], + ["time", PRTimeDaysAgo(300)], + ]), + ]; + + const UNEXPIRED_VISITS = [ + new Map([ + ["uri", Services.io.newURI("https://test4.invalid")], + ["title", "Test history visit 4"], + ]), + new Map([ + ["uri", Services.io.newURI("https://test5.invalid")], + ["title", "Test history visit 5"], + ["time", PRTimeDaysAgo(160)], + ]), + new Map([ + ["uri", Services.io.newURI("https://test6.invalid")], + ["title", "Test history visit 6"], + ["time", PRTimeDaysAgo(50)], + ]), + new Map([ + ["uri", Services.io.newURI("https://test7.invalid")], + ["title", "Test history visit 7"], + ["time", PRTimeDaysAgo(0)], + ]), + ]; + + let fakeIEHistoryEnumerator = MockRegistrar.register( + "@mozilla.org/profile/migrator/iehistoryenumerator;1", + new nsSimpleEnumerator([...EXPIRED_VISITS, ...UNEXPIRED_VISITS]) + ); + registerCleanupFunction(() => { + MockRegistrar.unregister(fakeIEHistoryEnumerator); + }); + + let migrator = await MigrationUtils.getMigrator("ie"); + Assert.ok(await migrator.isSourceAvailable(), "Source is available"); + + await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY); + + for (let visit of EXPIRED_VISITS) { + let entry = await PlacesUtils.history.fetch(visit.get("uri").spec, { + includeVisits: true, + }); + Assert.equal(entry, null, "Should not have found an entry."); + } + + for (let visit of UNEXPIRED_VISITS) { + let entry = await PlacesUtils.history.fetch(visit.get("uri"), { + includeVisits: true, + }); + Assert.equal( + entry.url, + visit.get("uri").spec, + "Should have the correct URL" + ); + Assert.equal( + entry.title, + visit.get("title"), + "Should have the correct title" + ); + Assert.ok(!!entry.visits.length, "Should have some visits"); + } + + await PlacesUtils.history.clear(); +}); 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..83748d870d --- /dev/null +++ b/browser/components/migration/tests/unit/test_MigrationUtils_timedRetry.js @@ -0,0 +1,29 @@ +"use strict"; + +let tmpFile = FileUtils.getDir("TmpD", []); +let dbConn; + +add_task(async function setup() { + tmpFile.append("TestDB"); + dbConn = await Sqlite.openConnection({ path: tmpFile.path }); + + registerCleanupFunction(async () => { + await dbConn.close(); + await IOUtils.remove(tmpFile.path); + }); +}); + +add_task(async function testgetRowsFromDBWithoutLocksRetries() { + let deferred = Promise.withResolvers(); + let promise = MigrationUtils.getRowsFromDBWithoutLocks( + tmpFile.path, + "Temp DB", + "SELECT * FROM moz_temp_table", + deferred.promise + ); + await new Promise(resolve => do_timeout(50, resolve)); + dbConn + .execute("CREATE TABLE moz_temp_table (id INTEGER PRIMARY KEY)") + .then(deferred.resolve); + await promise; +}); diff --git a/browser/components/migration/tests/unit/test_PasswordFileMigrator.js b/browser/components/migration/tests/unit/test_PasswordFileMigrator.js new file mode 100644 index 0000000000..e22f207c5d --- /dev/null +++ b/browser/components/migration/tests/unit/test_PasswordFileMigrator.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PasswordFileMigrator } = ChromeUtils.importESModule( + "resource:///modules/FileMigrators.sys.mjs" +); +const { LoginCSVImport } = ChromeUtils.importESModule( + "resource://gre/modules/LoginCSVImport.sys.mjs" +); +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); +const { MigrationWizardConstants } = ChromeUtils.importESModule( + "chrome://browser/content/migration/migration-wizard-constants.mjs" +); + +add_setup(async function () { + Services.prefs.setBoolPref("signon.management.page.fileImport.enabled", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("signon.management.page.fileImport.enabled"); + }); +}); + +/** + * Tests that the PasswordFileMigrator properly subclasses FileMigratorBase + * and delegates to the LoginCSVImport module. + */ +add_task(async function test_PasswordFileMigrator() { + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + let migrator = new PasswordFileMigrator(); + Assert.ok( + migrator.constructor.key, + "PasswordFileMigrator implements static getter 'key'" + ); + Assert.ok( + migrator.constructor.displayNameL10nID, + "PasswordFileMigrator implements static getter 'displayNameL10nID'" + ); + Assert.ok( + await migrator.getFilePickerConfig(), + "PasswordFileMigrator returns something for getFilePickerConfig()" + ); + Assert.ok( + migrator.displayedResourceTypes, + "PasswordFileMigrator returns something for displayedResourceTypes" + ); + Assert.ok(migrator.enabled, "PasswordFileMigrator is enabled."); + + const IMPORT_SUMMARY = [ + { + result: "added", + }, + { + result: "added", + }, + { + result: "modified", + }, + ]; + const EXPECTED_SUCCESS_STATE = { + [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW]: + "2 added", + [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_UPDATED]: + "1 updated", + }; + const FAKE_PATH = "some/fake/path.csv"; + + let importFromCSVStub = sandbox + .stub(LoginCSVImport, "importFromCSV") + .callsFake(somePath => { + Assert.equal(somePath, FAKE_PATH, "Got expected path"); + return Promise.resolve(IMPORT_SUMMARY); + }); + let result = await migrator.migrate(FAKE_PATH); + + Assert.ok(importFromCSVStub.called, "The stub should have been called."); + Assert.deepEqual( + result, + EXPECTED_SUCCESS_STATE, + "Got back the expected success state." + ); + + sandbox.restore(); +}); + +/** + * Tests that the PasswordFileMigrator will throw an exception with a + * consistent error message if the LoginCSVImport function rejects. + */ +add_task(async function test_PasswordFileMigrator_exception() { + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + let migrator = new PasswordFileMigrator(); + + const FAKE_PATH = "some/fake/path.csv"; + + sandbox.stub(LoginCSVImport, "importFromCSV").callsFake(() => { + return Promise.reject("Some error"); + }); + + await Assert.rejects( + migrator.migrate(FAKE_PATH), + /The file doesn’t include any valid password data/ + ); + + sandbox.restore(); +}); 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..85be9f0049 --- /dev/null +++ b/browser/components/migration/tests/unit/test_Safari_bookmarks.js @@ -0,0 +1,85 @@ +"use strict"; + +const { CustomizableUI } = ChromeUtils.importESModule( + "resource:///modules/CustomizableUI.sys.mjs" +); + +add_task(async function () { + registerFakePath("ULibDir", do_get_file("Library/")); + const faviconPath = do_get_file( + "Library/Safari/Favicon Cache/favicons.db" + ).path; + + 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 == "Food and Travel" + ) { + 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, 14, "Should import all 14 items."); + // Check that the telemetry matches: + Assert.equal( + MigrationUtils._importQuantities.bookmarks, + itemCount, + "Telemetry reporting correct." + ); + + // Check that favicons migrated + let faviconURIs = await MigrationUtils.getRowsFromDBWithoutLocks( + faviconPath, + "Safari Bookmark Favicons", + `SELECT I.uuid, I.url AS favicon_url, P.url + FROM icon_info I + INNER JOIN page_url P ON I.uuid = P.uuid;` + ); + let pageUrls = Array.from(faviconURIs, row => + Services.io.newURI(row.getResultByName("url")) + ); + await assertFavicons(pageUrls); + Assert.ok(observerNotified, "The observer should be notified upon migration"); +}); diff --git a/browser/components/migration/tests/unit/test_Safari_history.js b/browser/components/migration/tests/unit/test_Safari_history.js new file mode 100644 index 0000000000..c5b1210073 --- /dev/null +++ b/browser/components/migration/tests/unit/test_Safari_history.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const HISTORY_TEMPLATE_FILE_PATH = "Library/Safari/HistoryTemplate.db"; +const HISTORY_FILE_PATH = "Library/Safari/History.db"; + +// We want this to be some recent time, so we'll always add some time to our +// dates to keep them ~ five days ago. +const MS_FROM_REFERENCE_TIME = + new Date() - new Date("May 31, 2023 00:00:00 UTC"); + +const TEST_URLS = [ + { + url: "http://example.com/", + title: "Example Domain", + time: 706743588.04751, + jsTime: 1685050788047 + MS_FROM_REFERENCE_TIME, + visits: 1, + }, + { + url: "http://mozilla.org/", + title: "", + time: 706743581.133386, + jsTime: 1685050781133 + MS_FROM_REFERENCE_TIME, + visits: 1, + }, + { + url: "https://www.mozilla.org/en-CA/", + title: "Internet for people, not profit - Mozilla", + time: 706743581.133679, + jsTime: 1685050781133 + MS_FROM_REFERENCE_TIME, + visits: 1, + }, +]; + +async function setupHistoryFile() { + removeHistoryFile(); + let file = do_get_file(HISTORY_TEMPLATE_FILE_PATH); + file.copyTo(file.parent, "History.db"); + await updateVisitTimes(); +} + +function removeHistoryFile() { + let file = do_get_file(HISTORY_FILE_PATH, 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; + } + } +} + +async function updateVisitTimes() { + let cocoaDifference = MS_FROM_REFERENCE_TIME / 1000; + let historyFile = do_get_file(HISTORY_FILE_PATH); + let dbConn = await Sqlite.openConnection({ path: historyFile.path }); + await dbConn.execute( + "UPDATE history_visits SET visit_time = visit_time + :difference;", + { difference: cocoaDifference } + ); + await dbConn.close(); +} + +add_setup(async function setup() { + registerFakePath("ULibDir", do_get_file("Library/")); + await setupHistoryFile(); + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + removeHistoryFile(); + }); +}); + +add_task(async function testHistoryImport() { + await PlacesUtils.history.clear(); + + let migrator = await MigrationUtils.getMigrator("safari"); + await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY); + + for (let urlInfo of TEST_URLS) { + let entry = await PlacesUtils.history.fetch(urlInfo.url, { + includeVisits: true, + }); + + 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, + "Should have the correct number of visits" + ); + Assert.equal( + entry.visits[0].date.getTime(), + urlInfo.jsTime, + "Should have the correct date" + ); + } +}); diff --git a/browser/components/migration/tests/unit/test_Safari_history_strange_entries.js b/browser/components/migration/tests/unit/test_Safari_history_strange_entries.js new file mode 100644 index 0000000000..2578353e35 --- /dev/null +++ b/browser/components/migration/tests/unit/test_Safari_history_strange_entries.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PlacesQuery } = ChromeUtils.importESModule( + "resource://gre/modules/PlacesQuery.sys.mjs" +); + +const HISTORY_FILE_PATH = "Library/Safari/History.db"; +const HISTORY_STRANGE_ENTRIES_FILE_PATH = + "Library/Safari/HistoryStrangeEntries.db"; + +// By default, our migrators will cut off migrating any history older than +// 180 days. In order to make sure this test continues to run correctly +// in the future, we copy the reference database to History.db, and then +// use Sqlite.sys.mjs to connect to it and manually update all of the visit +// times to be "now", so that they all fall within the 180 day window. The +// Nov 10th date below is right around when the reference database visit +// entries were created. +// +// This update occurs in `updateVisitTimes`. +const MS_SINCE_SNAPSHOT_TIME = + new Date() - new Date("Nov 10, 2022 00:00:00 UTC"); + +async function setupHistoryFile() { + removeHistoryFile(); + let file = do_get_file(HISTORY_STRANGE_ENTRIES_FILE_PATH); + file.copyTo(file.parent, "History.db"); + await updateVisitTimes(); +} + +function removeHistoryFile() { + let file = do_get_file(HISTORY_FILE_PATH, 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_setup(async function setup() { + registerFakePath("ULibDir", do_get_file("Library/")); + await setupHistoryFile(); + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + removeHistoryFile(); + }); +}); + +async function updateVisitTimes() { + let cocoaSnapshotDelta = MS_SINCE_SNAPSHOT_TIME / 1000; + let historyFile = do_get_file(HISTORY_FILE_PATH); + let dbConn = await Sqlite.openConnection({ path: historyFile.path }); + + await dbConn.execute( + "UPDATE history_visits SET visit_time = visit_time + :cocoaSnapshotDelta;", + { + cocoaSnapshotDelta, + } + ); + + await dbConn.close(); +} + +/** + * Tests that we can import successfully from Safari when Safari's history + * database contains malformed URLs. + */ +add_task(async function testHistoryImportStrangeEntries() { + await PlacesUtils.history.clear(); + + let placesQuery = new PlacesQuery(); + let emptyHistory = await placesQuery.getHistory(); + Assert.equal(emptyHistory.size, 0, "Empty history should indeed be empty."); + + const EXPECTED_MIGRATED_SITES = 10; + const EXPECTED_MIGRATED_VISTS = 23; + + let historyFile = do_get_file(HISTORY_FILE_PATH); + let dbConn = await Sqlite.openConnection({ path: historyFile.path }); + let [rowCountResult] = await dbConn.execute( + "SELECT COUNT(*) FROM history_visits" + ); + Assert.greater( + rowCountResult.getResultByName("COUNT(*)"), + EXPECTED_MIGRATED_VISTS, + "There are more total rows than valid rows" + ); + await dbConn.close(); + + let migrator = await MigrationUtils.getMigrator("safari"); + await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY); + let migratedHistory = await placesQuery.getHistory({ sortBy: "site" }); + let siteCount = migratedHistory.size; + let visitCount = 0; + for (let [, visits] of migratedHistory) { + visitCount += visits.length; + } + Assert.equal( + siteCount, + EXPECTED_MIGRATED_SITES, + "Should have migrated all valid history sites" + ); + Assert.equal( + visitCount, + EXPECTED_MIGRATED_VISTS, + "Should have migrated all valid history visits" + ); + + placesQuery.close(); +}); diff --git a/browser/components/migration/tests/unit/test_Safari_permissions.js b/browser/components/migration/tests/unit/test_Safari_permissions.js new file mode 100644 index 0000000000..eaa6c7788e --- /dev/null +++ b/browser/components/migration/tests/unit/test_Safari_permissions.js @@ -0,0 +1,145 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if the migrator does not have the permission to + * read from the Safari data directory, that it can request + * permission to read from it, if the system allows. + */ + +const { SafariProfileMigrator } = ChromeUtils.importESModule( + "resource:///modules/SafariProfileMigrator.sys.mjs" +); + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +const { MockFilePicker } = ChromeUtils.importESModule( + "resource://testing-common/MockFilePicker.sys.mjs" +); + +let gDataDir; + +add_setup(async () => { + let tempDir = do_get_tempdir(); + gDataDir = PathUtils.join(tempDir.path, "Safari"); + await IOUtils.makeDirectory(gDataDir); + + registerFakePath("ULibDir", tempDir); + + MockFilePicker.init(globalThis); + registerCleanupFunction(() => { + MockFilePicker.cleanup(); + }); +}); + +/** + * Tests that canGetPermissions will return false if the platform does + * not allow for folder selection in the native file picker, and returns + * the data path otherwise. + */ +add_task(async function test_canGetPermissions() { + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + let migrator = new SafariProfileMigrator(); + // Not being able to get a folder picker is not a problem on macOS, but + // we'll test that case anyways. + let canGetPermissionsStub = sandbox + .stub(MigrationUtils, "canGetPermissionsOnPlatform") + .resolves(false); + + Assert.ok( + !(await migrator.canGetPermissions()), + "Should not be able to get permissions." + ); + + canGetPermissionsStub.resolves(true); + + Assert.equal( + await migrator.canGetPermissions(), + gDataDir, + "Should be able to get the permissions path." + ); + + sandbox.restore(); +}); + +/** + * Tests that getPermissions will show the native file picker in a + * loop until either the user cancels or selects a folder that grants + * read permissions to the data directory. + */ +add_task(async function test_getPermissions() { + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + let migrator = new SafariProfileMigrator(); + sandbox.stub(MigrationUtils, "canGetPermissionsOnPlatform").resolves(true); + let hasPermissionsStub = sandbox + .stub(migrator, "hasPermissions") + .resolves(false); + + Assert.equal( + await migrator.canGetPermissions(), + gDataDir, + "Should be able to get the permissions path." + ); + + let filePickerSeenCount = 0; + + let filePickerShownPromise = new Promise(resolve => { + MockFilePicker.showCallback = () => { + Assert.ok(true, "Filepicker shown."); + MockFilePicker.useDirectory([gDataDir]); + filePickerSeenCount++; + if (filePickerSeenCount > 3) { + Assert.ok(true, "File picker looped 3 times."); + hasPermissionsStub.resolves(true); + resolve(); + } + }; + }); + MockFilePicker.returnValue = MockFilePicker.returnOK; + + // This is a little awkward, but we need to ensure that the + // filePickerShownPromise resolves first before we await + // the getPermissionsPromise in order to get the correct + // filePickerSeenCount. + let getPermissionsPromise = migrator.getPermissions(); + await filePickerShownPromise; + Assert.ok( + await getPermissionsPromise, + "Should report that we got permissions." + ); + + // Make sure that the user can also hit "cancel" and that we + // file picker loop. + + hasPermissionsStub.resolves(false); + + filePickerSeenCount = 0; + filePickerShownPromise = new Promise(resolve => { + MockFilePicker.showCallback = () => { + Assert.ok(true, "Filepicker shown."); + filePickerSeenCount++; + Assert.equal(filePickerSeenCount, 1, "Saw the picker once."); + resolve(); + }; + }); + MockFilePicker.returnValue = MockFilePicker.returnCancel; + Assert.ok( + !(await migrator.getPermissions()), + "Should report that we didn't get permissions." + ); + await filePickerShownPromise; + + sandbox.restore(); +}); 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..68e34beab3 --- /dev/null +++ b/browser/components/migration/tests/unit/test_fx_telemetry.js @@ -0,0 +1,436 @@ +/* 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" +); +const { InternalTestingProfileMigrator } = ChromeUtils.importESModule( + "resource:///modules/InternalTestingProfileMigrator.sys.mjs" +); +const { LoginCSVImport } = ChromeUtils.importESModule( + "resource://gre/modules/LoginCSVImport.sys.mjs" +); +const { PasswordFileMigrator } = ChromeUtils.importESModule( + "resource:///modules/FileMigrators.sys.mjs" +); +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +// These preferences are set to true anytime MigratorBase.migrate +// successfully completes a migration of their type. +const BOOKMARKS_PREF = "browser.migrate.interactions.bookmarks"; +const CSV_PASSWORDS_PREF = "browser.migrate.interactions.csvpasswords"; +const HISTORY_PREF = "browser.migrate.interactions.history"; +const PASSWORDS_PREF = "browser.migrate.interactions.passwords"; + +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); +}); + +/** + * Tests that when importing bookmarks, history, or passwords, we + * set interaction prefs. These preferences are sent using + * TelemetryEnvironment.sys.mjs. + */ +add_task(async function test_interaction_telemetry() { + let testingMigrator = await MigrationUtils.getMigrator( + InternalTestingProfileMigrator.key + ); + + Services.prefs.clearUserPref(BOOKMARKS_PREF); + Services.prefs.clearUserPref(HISTORY_PREF); + Services.prefs.clearUserPref(PASSWORDS_PREF); + + // Ensure that these prefs start false. + Assert.ok(!Services.prefs.getBoolPref(BOOKMARKS_PREF)); + Assert.ok(!Services.prefs.getBoolPref(HISTORY_PREF)); + Assert.ok(!Services.prefs.getBoolPref(PASSWORDS_PREF)); + + await testingMigrator.migrate( + MigrationUtils.resourceTypes.BOOKMARKS, + false, + InternalTestingProfileMigrator.testProfile + ); + Assert.ok( + Services.prefs.getBoolPref(BOOKMARKS_PREF), + "Bookmarks pref should have been set." + ); + Assert.ok(!Services.prefs.getBoolPref(HISTORY_PREF)); + Assert.ok(!Services.prefs.getBoolPref(PASSWORDS_PREF)); + + await testingMigrator.migrate( + MigrationUtils.resourceTypes.HISTORY, + false, + InternalTestingProfileMigrator.testProfile + ); + Assert.ok( + Services.prefs.getBoolPref(BOOKMARKS_PREF), + "Bookmarks pref should have been set." + ); + Assert.ok( + Services.prefs.getBoolPref(HISTORY_PREF), + "History pref should have been set." + ); + Assert.ok(!Services.prefs.getBoolPref(PASSWORDS_PREF)); + + await testingMigrator.migrate( + MigrationUtils.resourceTypes.PASSWORDS, + false, + InternalTestingProfileMigrator.testProfile + ); + Assert.ok( + Services.prefs.getBoolPref(BOOKMARKS_PREF), + "Bookmarks pref should have been set." + ); + Assert.ok( + Services.prefs.getBoolPref(HISTORY_PREF), + "History pref should have been set." + ); + Assert.ok( + Services.prefs.getBoolPref(PASSWORDS_PREF), + "Passwords pref should have been set." + ); + + // Now make sure that we still record these if we migrate a + // series of resources at the same time. + Services.prefs.clearUserPref(BOOKMARKS_PREF); + Services.prefs.clearUserPref(HISTORY_PREF); + Services.prefs.clearUserPref(PASSWORDS_PREF); + + await testingMigrator.migrate( + MigrationUtils.resourceTypes.ALL, + false, + InternalTestingProfileMigrator.testProfile + ); + Assert.ok( + Services.prefs.getBoolPref(BOOKMARKS_PREF), + "Bookmarks pref should have been set." + ); + Assert.ok( + Services.prefs.getBoolPref(HISTORY_PREF), + "History pref should have been set." + ); + Assert.ok( + Services.prefs.getBoolPref(PASSWORDS_PREF), + "Passwords pref should have been set." + ); +}); + +/** + * Tests that when importing passwords from a CSV file using the + * migration wizard, we set an interaction pref. This preference + * is sent using TelemetryEnvironment.sys.mjs. + */ +add_task(async function test_csv_password_interaction_telemetry() { + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + + let testingMigrator = new PasswordFileMigrator(); + + Services.prefs.clearUserPref(CSV_PASSWORDS_PREF); + Assert.ok(!Services.prefs.getBoolPref(CSV_PASSWORDS_PREF)); + + sandbox.stub(LoginCSVImport, "importFromCSV").resolves([]); + await testingMigrator.migrate("some/fake/path.csv"); + + Assert.ok( + Services.prefs.getBoolPref(CSV_PASSWORDS_PREF), + "CSV import pref should have been set." + ); + + sandbox.restore(); +}); + +/** + * Tests that interaction preferences used for TelemetryEnvironment are + * persisted across profile resets. + */ +add_task(async function test_interaction_telemetry_persist_across_reset() { + const PREFS = ` +user_pref("${BOOKMARKS_PREF}", true); +user_pref("${CSV_PASSWORDS_PREF}", true); +user_pref("${HISTORY_PREF}", true); +user_pref("${PASSWORDS_PREF}", true); + `; + + let [srcDir, targetDir] = getTestDirs(); + writeToFile(srcDir, "prefs.js", PREFS); + + let ok = await promiseTelemetryMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true"); + + let prefsPath = PathUtils.join(targetDir.path, "prefs.js"); + Assert.ok(await IOUtils.exists(prefsPath), "Prefs should have been written."); + let writtenPrefsString = await IOUtils.readUTF8(prefsPath); + for (let prefKey of [ + BOOKMARKS_PREF, + CSV_PASSWORDS_PREF, + HISTORY_PREF, + PASSWORDS_PREF, + ]) { + const EXPECTED = `user_pref("${prefKey}", true);`; + Assert.ok(writtenPrefsString.includes(EXPECTED), "Found persisted pref."); + } +}); diff --git a/browser/components/migration/tests/unit/xpcshell.toml b/browser/components/migration/tests/unit/xpcshell.toml new file mode 100644 index 0000000000..b599a64362 --- /dev/null +++ b/browser/components/migration/tests/unit/xpcshell.toml @@ -0,0 +1,95 @@ +[DEFAULT] +head = "head_migration.js" +tags = "condprof" +firefox-appdir = "browser" +skip-if = ["os == 'android'"] # bug 1730213 +prefs = ["browser.migrate.showBookmarksToolbarAfterMigration=true"] +support-files = [ + "Library/**", + "AppData/**", + "bookmarks.exported.html", + "bookmarks.exported.json", + "bookmarks.invalid.html", +] + +["test_360seMigrationUtils.js"] +run-if = ["os == 'win'"] + +["test_360se_bookmarks.js"] +run-if = ["os == 'win'"] + +["test_BookmarksFileMigrator.js"] + +["test_ChromeMigrationUtils.js"] + +["test_ChromeMigrationUtils_path.js"] + +["test_ChromeMigrationUtils_path_chromium_snap.js"] +run-if = ["os == 'linux'"] + +["test_Chrome_bookmarks.js"] + +["test_Chrome_corrupt_history.js"] + +["test_Chrome_credit_cards.js"] +skip-if = [ + "os == 'linux'", + "os == 'android'", + "condprof", # bug 1769154 - not realistic for condprof +] + +["test_Chrome_extensions.js"] + +["test_Chrome_formdata.js"] + +["test_Chrome_history.js"] +skip-if = ["os != 'mac'"] # Relies on ULibDir + +["test_Chrome_passwords.js"] +skip-if = [ + "os == 'linux'", + "os == 'android'", + "condprof", # bug 1769154 - not realistic for condprof +] + +["test_Chrome_passwords_emptySource.js"] +skip-if = [ + "os == 'linux'", + "os == 'android'", + "condprof", # bug 1769154 - not realistic for condprof +] +support-files = ["LibraryWithNoData/**"] + +["test_Chrome_permissions.js"] + +["test_Edge_db_migration.js"] +run-if = ["os == 'win'"] + +["test_Edge_registry_migration.js"] +run-if = ["os == 'win'"] + +["test_IE_bookmarks.js"] +run-if = ["os == 'win' && bits == 64"] # bug 1392396 + +["test_IE_history.js"] +run-if = ["os == 'win'"] +skip-if = ["os == 'win' && msix"] # https://bugzilla.mozilla.org/show_bug.cgi?id=1807928 + +["test_MigrationUtils_timedRetry.js"] +skip-if = ["os == 'mac' && !debug"] #Bug 1558330 + +["test_PasswordFileMigrator.js"] + +["test_Safari_bookmarks.js"] +run-if = ["os == 'mac'"] + +["test_Safari_history.js"] +run-if = ["os == 'mac'"] + +["test_Safari_history_strange_entries.js"] +run-if = ["os == 'mac'"] + +["test_Safari_permissions.js"] +run-if = ["os == 'mac'"] + +["test_fx_telemetry.js"] |