diff options
Diffstat (limited to 'browser/components/backup/tests/marionette/test_backup.py')
-rw-r--r-- | browser/components/backup/tests/marionette/test_backup.py | 713 |
1 files changed, 713 insertions, 0 deletions
diff --git a/browser/components/backup/tests/marionette/test_backup.py b/browser/components/backup/tests/marionette/test_backup.py new file mode 100644 index 0000000000..3b11b50ae8 --- /dev/null +++ b/browser/components/backup/tests/marionette/test_backup.py @@ -0,0 +1,713 @@ +# 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/. + +import json +import os +import shutil +import tempfile + +import mozfile +from marionette_harness import MarionetteTestCase + + +class BackupTest(MarionetteTestCase): + # This is the DB key that will be computed for the http2-ca.pem certificate + # that's included in a support-file for this test. + _cert_db_key = "AAAAAAAAAAAAAAAUAAAAG0Wbze8lahTcE4RhwEqMtTpThrzjMBkxFzAVBgNVBAMMDiBIVFRQMiBUZXN0IENB" + + def setUp(self): + MarionetteTestCase.setUp(self) + # We need to quit the browser and restart with the browser.backup.log + # pref already set to true in order for it to be displayed. + self.marionette.quit() + self.marionette.instance.prefs = { + "browser.backup.log": True, + } + # Now restart the browser. + self.marionette.instance.switch_profile() + self.marionette.start_session() + + def test_backup(self): + self.marionette.set_context("chrome") + + self.add_test_cookie() + self.add_test_login() + self.add_test_certificate() + self.add_test_saved_address() + self.add_test_identity_credential() + self.add_test_form_history() + self.add_test_activity_stream_snippets_data() + self.add_test_protections_data() + self.add_test_bookmarks() + self.add_test_history() + self.add_test_preferences() + self.add_test_permissions() + + resourceKeys = self.marionette.execute_script( + """ + const DefaultBackupResources = ChromeUtils.importESModule("resource:///modules/backup/BackupResources.sys.mjs"); + let resourceKeys = []; + for (const resourceName in DefaultBackupResources) { + let resource = DefaultBackupResources[resourceName]; + resourceKeys.push(resource.key); + } + return resourceKeys; + """ + ) + + originalStagingPath = self.marionette.execute_async_script( + """ + const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs"); + let bs = BackupService.init(); + if (!bs) { + throw new Error("Could not get initialized BackupService."); + } + + let [outerResolve] = arguments; + (async () => { + let { stagingPath } = await bs.createBackup(); + if (!stagingPath) { + throw new Error("Could not create backup."); + } + return stagingPath; + })().then(outerResolve); + """ + ) + + # When we switch over to the recovered profile, the Marionette framework + # will blow away the profile directory of the one that we created the + # backup on, which ruins our ability to do postRecovery work, since + # that relies on the prior profile sticking around. We work around this + # by moving the staging folder we got back to the OS temporary + # directory, and telling the recovery method to use that instead of the + # one from the profile directory. + stagingPath = os.path.join(tempfile.gettempdir(), "staging-test") + # Delete the destination folder if it exists already + shutil.rmtree(stagingPath, ignore_errors=True) + shutil.move(originalStagingPath, stagingPath) + + # First, ensure that the staging path exists + self.assertTrue(os.path.exists(stagingPath)) + # Now, ensure that the backup-manifest.json file exists within it. + manifestPath = os.path.join(stagingPath, "backup-manifest.json") + self.assertTrue(os.path.exists(manifestPath)) + + # For now, we just do a cursory check to ensure that for the resources + # that are listed in the manifest as having been backed up, that we + # have at least one file in their respective staging directories. + # We don't check the contents of the files, just that they exist. + + # Read the JSON manifest file + with open(manifestPath, "r") as f: + manifest = json.load(f) + + # Ensure that the manifest has a "resources" key + self.assertIn("resources", manifest) + resources = manifest["resources"] + self.assertTrue(isinstance(resources, dict)) + self.assertTrue(len(resources) > 0) + + # We don't have encryption capabilities wired up yet, so we'll check + # that all default resources are represented in the manifest. + self.assertEqual(len(resources), len(resourceKeys)) + for resourceKey in resourceKeys: + self.assertIn(resourceKey, resources) + + # Iterate the resources dict keys + for resourceKey in resources: + print("Checking resource: %s" % resourceKey) + # Ensure that there are staging directories created for each + # resource that was backed up + resourceStagingDir = os.path.join(stagingPath, resourceKey) + self.assertTrue(os.path.exists(resourceStagingDir)) + + # Start a brand new profile, one without any of the data we created or + # backed up. This is the one that we'll be starting recovery from. + self.marionette.quit() + self.marionette.instance.profile = None + self.marionette.start_session() + self.marionette.set_context("chrome") + + # Recover the created backup into a new profile directory. Also get out + # the client ID of this profile, because we're going to want to make + # sure that this client ID is inherited by the recovered profile. + [ + newProfileName, + newProfilePath, + expectedClientID, + ] = self.marionette.execute_async_script( + """ + const { ClientID } = ChromeUtils.importESModule("resource://gre/modules/ClientID.sys.mjs"); + const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs"); + let bs = BackupService.get(); + if (!bs) { + throw new Error("Could not get initialized BackupService."); + } + + let [stagingPath, outerResolve] = arguments; + (async () => { + let newProfileRootPath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "recoverFromBackupTest-newProfileRoot" + ); + let newProfile = await bs.recoverFromBackup(stagingPath, false, newProfileRootPath) + if (!newProfile) { + throw new Error("Could not create recovery profile."); + } + + let expectedClientID = await ClientID.getClientID(); + + return [newProfile.name, newProfile.rootDir.path, expectedClientID]; + })().then(outerResolve); + """, + script_args=[stagingPath], + ) + + print("Recovery name: %s" % newProfileName) + print("Recovery path: %s" % newProfilePath) + print("Expected clientID: %s" % expectedClientID) + + self.marionette.quit() + originalProfile = self.marionette.instance.profile + self.marionette.instance.profile = newProfilePath + self.marionette.start_session() + self.marionette.set_context("chrome") + + # Ensure that all postRecovery actions have completed. + self.marionette.execute_async_script( + """ + const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs"); + let bs = BackupService.get(); + if (!bs) { + throw new Error("Could not get initialized BackupService."); + } + + let [outerResolve] = arguments; + (async () => { + await bs.postRecoveryComplete; + })().then(outerResolve); + """ + ) + + self.verify_recovered_test_cookie() + self.verify_recovered_test_login() + self.verify_recovered_test_certificate() + self.verify_recovered_saved_address() + self.verify_recovered_identity_credential() + self.verify_recovered_form_history() + self.verify_recovered_activity_stream_snippets_data() + self.verify_recovered_protections_data() + self.verify_recovered_bookmarks() + self.verify_recovered_history() + self.verify_recovered_preferences() + self.verify_recovered_permissions() + + # Now also ensure that the recovered profile inherited the client ID + # from the profile that initiated recovery. + recoveredClientID = self.marionette.execute_async_script( + """ + const { ClientID } = ChromeUtils.importESModule("resource://gre/modules/ClientID.sys.mjs"); + let [outerResolve] = arguments; + (async () => { + return ClientID.getClientID(); + })().then(outerResolve); + """ + ) + self.assertEqual(recoveredClientID, expectedClientID) + + # Try not to pollute the profile list by getting rid of the one we just + # created. + self.marionette.quit() + self.marionette.instance.profile = originalProfile + self.marionette.start_session() + self.marionette.set_context("chrome") + self.marionette.execute_script( + """ + let newProfileName = arguments[0]; + let profileSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService( + Ci.nsIToolkitProfileService + ); + let profile = profileSvc.getProfileByName(newProfileName); + profile.remove(true); + profileSvc.flush(); + """, + script_args=[newProfileName], + ) + + # Cleanup the staging path that we moved + mozfile.remove(stagingPath) + + def add_test_cookie(self): + self.marionette.execute_async_script( + """ + let [outerResolve] = arguments; + (async () => { + // We'll just add a single cookie, and then make sure that it shows + // up on the other side. + Services.cookies.removeAll(); + Services.cookies.add( + ".example.com", + "/", + "first", + "one", + false, + false, + false, + Date.now() / 1000 + 1, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTP + ); + })().then(outerResolve); + """ + ) + + def verify_recovered_test_cookie(self): + cookiesLength = self.marionette.execute_async_script( + """ + let [outerResolve] = arguments; + (async () => { + let cookies = Services.cookies.getCookiesFromHost("example.com", {}); + return cookies.length; + })().then(outerResolve); + """ + ) + self.assertEqual(cookiesLength, 1) + + def add_test_login(self): + self.marionette.execute_async_script( + """ + let [outerResolve] = arguments; + (async () => { + // Let's start with adding a single password + Services.logins.removeAllLogins(); + + const nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, + "init" + ); + + const login1 = new nsLoginInfo( + "https://example.com", + "https://example.com", + null, + "notifyu1", + "notifyp1", + "user", + "pass" + ); + await Services.logins.addLoginAsync(login1); + })().then(outerResolve); + """ + ) + + def verify_recovered_test_login(self): + loginsLength = self.marionette.execute_async_script( + """ + let [outerResolve] = arguments; + (async () => { + let logins = await Services.logins.searchLoginsAsync({ + origin: "https://example.com", + }); + return logins.length; + })().then(outerResolve); + """ + ) + self.assertEqual(loginsLength, 1) + + def add_test_certificate(self): + certPath = os.path.join(os.path.dirname(__file__), "http2-ca.pem") + self.marionette.execute_async_script( + """ + let [certPath, certDbKey, outerResolve] = arguments; + (async () => { + const { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" + ); + + let certDb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + + if (certDb.findCertByDBKey(certDbKey)) { + throw new Error("Should not have this certificate yet!"); + } + + let certFile = await IOUtils.getFile(certPath); + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(certFile, -1, 0, 0); + let data = NetUtil.readInputStreamToString(fstream, fstream.available()); + fstream.close(); + + let pem = data.replace(/-----BEGIN CERTIFICATE-----/, "") + .replace(/-----END CERTIFICATE-----/, "") + .replace(/[\\r\\n]/g, ""); + let cert = certDb.addCertFromBase64(pem, "CTu,u,u"); + + if (cert.dbKey != certDbKey) { + throw new Error("The inserted certificate DB key is unexpected."); + } + })().then(outerResolve); + """, + script_args=[certPath, self._cert_db_key], + ) + + def verify_recovered_test_certificate(self): + certExists = self.marionette.execute_async_script( + """ + let [certDbKey, outerResolve] = arguments; + (async () => { + let certDb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + return certDb.findCertByDBKey(certDbKey) != null; + })().then(outerResolve); + """, + script_args=[self._cert_db_key], + ) + self.assertTrue(certExists) + + def add_test_saved_address(self): + self.marionette.execute_async_script( + """ + const { formAutofillStorage } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillStorage.sys.mjs" + ); + + let [outerResolve] = arguments; + (async () => { + 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", + }; + await formAutofillStorage.initialize(); + formAutofillStorage.addresses.removeAll(); + await formAutofillStorage.addresses.add(TEST_ADDRESS_1); + })().then(outerResolve); + """ + ) + + def verify_recovered_saved_address(self): + addressesLength = self.marionette.execute_async_script( + """ + const { formAutofillStorage } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillStorage.sys.mjs" + ); + + let [outerResolve] = arguments; + (async () => { + await formAutofillStorage.initialize(); + let addresses = await formAutofillStorage.addresses.getAll(); + return addresses.length; + })().then(outerResolve); + """ + ) + self.assertEqual(addressesLength, 1) + + def add_test_identity_credential(self): + self.marionette.execute_async_script( + """ + let [outerResolve] = arguments; + (async () => { + let service = Cc["@mozilla.org/browser/identity-credential-storage-service;1"] + .getService(Ci.nsIIdentityCredentialStorageService); + service.clear(); + + let testPrincipal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI("https://test.com/"), + {} + ); + let idpPrincipal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI("https://idp-test.com/"), + {} + ); + + service.setState( + testPrincipal, + idpPrincipal, + "ID", + true, + true + ); + + })().then(outerResolve); + """ + ) + + def verify_recovered_identity_credential(self): + [registered, allowLogout] = self.marionette.execute_async_script( + """ + let [outerResolve] = arguments; + (async () => { + let service = Cc["@mozilla.org/browser/identity-credential-storage-service;1"] + .getService(Ci.nsIIdentityCredentialStorageService); + + let testPrincipal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI("https://test.com/"), + {} + ); + let idpPrincipal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI("https://idp-test.com/"), + {} + ); + + let registered = {}; + let allowLogout = {}; + + service.getState( + testPrincipal, + idpPrincipal, + "ID", + registered, + allowLogout + ); + + return [registered.value, allowLogout.value]; + })().then(outerResolve); + """ + ) + self.assertTrue(registered) + self.assertTrue(allowLogout) + + def add_test_form_history(self): + self.marionette.execute_async_script( + """ + const { FormHistory } = ChromeUtils.importESModule( + "resource://gre/modules/FormHistory.sys.mjs" + ); + + let [outerResolve] = arguments; + (async () => { + await FormHistory.update({ + op: "add", + fieldname: "some-test-field", + value: "I was recovered!", + timesUsed: 1, + firstUsed: 0, + lastUsed: 0, + }); + + })().then(outerResolve); + """ + ) + + def verify_recovered_form_history(self): + formHistoryResultsLength = self.marionette.execute_async_script( + """ + const { FormHistory } = ChromeUtils.importESModule( + "resource://gre/modules/FormHistory.sys.mjs" + ); + + let [outerResolve] = arguments; + (async () => { + let results = await FormHistory.search( + ["guid"], + { fieldname: "some-test-field" } + ); + return results.length; + })().then(outerResolve); + """ + ) + self.assertEqual(formHistoryResultsLength, 1) + + def add_test_activity_stream_snippets_data(self): + self.marionette.execute_async_script( + """ + const { ActivityStreamStorage } = ChromeUtils.importESModule( + "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs", + ); + const SNIPPETS_TABLE_NAME = "snippets"; + + let [outerResolve] = arguments; + (async () => { + let storage = new ActivityStreamStorage({ + storeNames: [SNIPPETS_TABLE_NAME], + }); + let snippetsTable = await storage.getDbTable(SNIPPETS_TABLE_NAME); + await snippetsTable.set("backup-test", "some-test-value"); + })().then(outerResolve); + """ + ) + + def verify_recovered_activity_stream_snippets_data(self): + snippetsResult = self.marionette.execute_async_script( + """ + const { ActivityStreamStorage } = ChromeUtils.importESModule( + "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs", + ); + const SNIPPETS_TABLE_NAME = "snippets"; + + let [outerResolve] = arguments; + (async () => { + let storage = new ActivityStreamStorage({ + storeNames: [SNIPPETS_TABLE_NAME], + }); + let snippetsTable = await storage.getDbTable(SNIPPETS_TABLE_NAME); + return await snippetsTable.get("backup-test"); + })().then(outerResolve); + """ + ) + self.assertEqual(snippetsResult, "some-test-value") + + def add_test_protections_data(self): + self.marionette.execute_async_script( + """ + const TrackingDBService = Cc["@mozilla.org/tracking-db-service;1"] + .getService(Ci.nsITrackingDBService); + + let [outerResolve] = arguments; + (async () => { + let entry = { + "https://test.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1], + ], + }; + await TrackingDBService.clearAll(); + await TrackingDBService.saveEvents(JSON.stringify(entry)); + })().then(outerResolve); + """ + ) + + def verify_recovered_protections_data(self): + eventsSum = self.marionette.execute_async_script( + """ + const TrackingDBService = Cc["@mozilla.org/tracking-db-service;1"] + .getService(Ci.nsITrackingDBService); + + let [outerResolve] = arguments; + (async () => { + return TrackingDBService.sumAllEvents(); + })().then(outerResolve); + """ + ) + self.assertEqual(eventsSum, 1) + + def add_test_bookmarks(self): + self.marionette.execute_async_script( + """ + const { PlacesUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PlacesUtils.sys.mjs" + ); + + let [outerResolve] = arguments; + (async () => { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "Some test page", + url: Services.io.newURI("https://www.backup.test/"), + }); + })().then(outerResolve); + """ + ) + + def verify_recovered_bookmarks(self): + bookmarkExists = self.marionette.execute_async_script( + """ + const { PlacesUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PlacesUtils.sys.mjs" + ); + + let [outerResolve] = arguments; + (async () => { + let url = Services.io.newURI("https://www.backup.test/"); + let bookmark = await PlacesUtils.bookmarks.fetch({ url }); + return bookmark != null; + })().then(outerResolve); + """ + ) + self.assertTrue(bookmarkExists) + + def add_test_history(self): + self.marionette.execute_async_script( + """ + const { PlacesUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PlacesUtils.sys.mjs" + ); + + let [outerResolve] = arguments; + (async () => { + await PlacesUtils.history.clear(); + + let entry = { + url: "http://my-restored-history.com", + visits: [{ transition: PlacesUtils.history.TRANSITION_LINK }], + }; + + await PlacesUtils.history.insertMany([entry]); + })().then(outerResolve); + """ + ) + + def verify_recovered_history(self): + historyExists = self.marionette.execute_async_script( + """ + const { PlacesUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PlacesUtils.sys.mjs" + ); + + let [outerResolve] = arguments; + (async () => { + let entry = await PlacesUtils.history.fetch("http://my-restored-history.com"); + return entry != null; + })().then(outerResolve); + """ + ) + self.assertTrue(historyExists) + + def add_test_preferences(self): + self.marionette.execute_script( + """ + Services.prefs.setBoolPref("test-pref-for-backup", true) + """ + ) + + def verify_recovered_preferences(self): + prefExists = self.marionette.execute_script( + """ + return Services.prefs.getBoolPref("test-pref-for-backup", false); + """ + ) + self.assertTrue(prefExists) + + def add_test_permissions(self): + self.marionette.execute_script( + """ + let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://test-permission-site.com" + ); + Services.perms.addFromPrincipal( + principal, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + """ + ) + + def verify_recovered_permissions(self): + permissionExists = self.marionette.execute_script( + """ + let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://test-permission-site.com" + ); + let perms = Services.perms.getAllForPrincipal(principal); + if (perms.length != 1) { + throw new Error("Got an unexpected number of permissions"); + } + return perms[0].type == "desktop-notification" + """ + ) + self.assertTrue(permissionExists) |