tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_backup.py (34647B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 import os
      6 import shutil
      7 import tempfile
      8 
      9 import mozfile
     10 from marionette_harness import MarionetteTestCase
     11 
     12 
     13 class BackupTest(MarionetteTestCase):
     14    # This is the DB key that will be computed for the http2-ca.pem certificate
     15    # that's included in a support-file for this test.
     16    _cert_db_key = "AAAAAAAAAAAAAAAUAAAAG0Wbze8lahTcE4RhwEqMtTpThrzjMBkxFzAVBgNVBAMMDiBIVFRQMiBUZXN0IENB"
     17 
     18    def setUp(self):
     19        MarionetteTestCase.setUp(self)
     20 
     21        # We need to force the service to be enabled because it's disabled
     22        # by default for Marionette. Also "browser.backup.log" has to be set
     23        # to true before Firefox starts in order for it to be displayed.
     24        self.marionette.enforce_gecko_prefs({
     25            "browser.backup.enabled": True,
     26            "browser.backup.log": True,
     27            "browser.backup.archive.enabled": True,
     28            "browser.backup.restore.enabled": True,
     29            "browser.backup.archive.overridePlatformCheck": True,
     30            "browser.backup.restore.overridePlatformCheck": True,
     31            # Necessary to test Session Restore from backup, which relies on
     32            # the crash restore mechanism.
     33            "browser.sessionstore.resume_from_crash": True,
     34        })
     35 
     36        self.marionette.set_context("chrome")
     37 
     38    def tearDown(self):
     39        # Restart Firefox with a new profile to get rid from all modifications.
     40        self.marionette.quit()
     41        self.marionette.instance.switch_profile()
     42        self.marionette.start_session()
     43 
     44        MarionetteTestCase.tearDown(self)
     45 
     46    def test_backup(self):
     47        self.add_test_cookie()
     48        self.add_test_login()
     49        self.add_test_certificate()
     50        self.add_test_saved_address()
     51        self.add_test_identity_credential()
     52        self.add_test_form_history()
     53        self.add_test_asrouter_snippets_data()
     54        self.add_test_protections_data()
     55        self.add_test_bookmarks()
     56        self.add_test_history()
     57        self.add_test_preferences()
     58        self.add_test_permissions()
     59 
     60        # We want to make sure that any payment methods in this testing profile
     61        # are properly encrypted using OSKeyStore, and that the encrypted
     62        # backup will properly extract and recover from the original OSKeyStore
     63        # secret.
     64        #
     65        # What we _don't_ want to do is encrypt or extract the OSKeyStore secret
     66        # used by this machine's _actual_ Firefox instance, if one exists
     67        # (since they're all shared). We also definitely do not want to
     68        # accidentally overwrite that secret.
     69        #
     70        # We solve this by poking a new STORE_LABEL value into the OSKeyStore
     71        # module before we do the following:
     72        #
     73        # 1. Store payment methods
     74        # 2. Enable encryption
     75        #
     76        # Once that'd one, we delete the temporary OSKeyStore row that we
     77        # created. This technique is similar to the one used in
     78        # OSKeyStoreTestUtils, which is unfortunately not a module that is
     79        # available to Marionette tests.
     80        backupOSKeyStoreLabel = self.marionette.execute_script(
     81            """
     82          const { OSKeyStore } = ChromeUtils.importESModule(
     83            "resource://gre/modules/OSKeyStore.sys.mjs"
     84          );
     85 
     86          const BACKUP_OSKEYSTORE_LABEL = "test-" + Math.random().toString(36).substr(2);
     87          OSKeyStore.STORE_LABEL = BACKUP_OSKEYSTORE_LABEL;
     88          return BACKUP_OSKEYSTORE_LABEL;
     89        """
     90        )
     91 
     92        # Now that we've got the fake OSKeyStore set up, we can insert our
     93        # testing payment methods.
     94        self.add_test_payment_methods()
     95 
     96        # Restart the browser to force all of the test data we just added
     97        # to be flushed to disk and to be made ready for backup
     98        self.marionette.restart()
     99 
    100        # We want to validate that TabState is flushed before serializing the
    101        # backup, so run this test in the same browser instance we invoke the
    102        # backup in.
    103        self.add_test_sessionstore()
    104 
    105        # Put the OSKeyStore label back, since it would have been cleared
    106        # from memory during the restart.
    107        self.marionette.execute_script(
    108            """
    109          const { OSKeyStore } = ChromeUtils.importESModule(
    110            "resource://gre/modules/OSKeyStore.sys.mjs"
    111          );
    112 
    113          const BACKUP_OSKEYSTORE_LABEL = arguments[0];
    114          OSKeyStore.STORE_LABEL = BACKUP_OSKEYSTORE_LABEL;
    115        """,
    116            script_args=[backupOSKeyStoreLabel],
    117        )
    118 
    119        archiveDestPath = os.path.join(tempfile.gettempdir(), "backup-dest")
    120        recoveryCode = "This is a test password"
    121        archivePath = self.marionette.execute_async_script(
    122            """
    123 
    124          const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs");
    125          let bs = BackupService.init();
    126          if (!bs) {
    127            throw new Error("Could not get initialized BackupService.");
    128          }
    129 
    130          let [archiveDestPath, recoveryCode, outerResolve] = arguments;
    131          bs.setParentDirPath(archiveDestPath);
    132 
    133          (async () => {
    134 
    135            await bs.enableEncryption(recoveryCode);
    136 
    137            let { archivePath } = await bs.createBackup();
    138            if (!archivePath) {
    139              throw new Error("Could not create backup.");
    140            }
    141            return archivePath;
    142          })().then(outerResolve);
    143        """,
    144            script_args=[archiveDestPath, recoveryCode],
    145        )
    146 
    147        # Now we clean up our temporary OSKeyStore from the OS's secure storage.
    148        # We won't need it anymore.
    149        self.marionette.execute_async_script(
    150            """
    151           const { OSKeyStore } = ChromeUtils.importESModule(
    152             "resource://gre/modules/OSKeyStore.sys.mjs"
    153           );
    154 
    155           let [outerResolve] = arguments;
    156           (async () => {
    157              await OSKeyStore.cleanup();
    158           })().then(outerResolve);
    159        """
    160        )
    161 
    162        recoveryPath = os.path.join(tempfile.gettempdir(), "recovery")
    163        shutil.rmtree(recoveryPath, ignore_errors=True)
    164 
    165        # Start a brand new profile, one without any of the data we created or
    166        # backed up. This is the one that we'll be starting recovery from.
    167        self.marionette.quit()
    168        self.marionette.instance.switch_profile()
    169        self.marionette.start_session()
    170        self.marionette.set_context("chrome")
    171 
    172        # Recover the created backup into a new profile directory. Also get out
    173        # the client ID of this profile, because we're going to want to make
    174        # sure that this client ID is not inherited from the intermediate profile.
    175        [
    176            newProfileName,
    177            newProfilePath,
    178            intermediateClientID,
    179            osKeyStoreLabel,
    180        ] = self.marionette.execute_async_script(
    181            """
    182          const { OSKeyStore } = ChromeUtils.importESModule("resource://gre/modules/OSKeyStore.sys.mjs");
    183          const { ClientID } = ChromeUtils.importESModule("resource://gre/modules/ClientID.sys.mjs");
    184          const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs");
    185          let bs = BackupService.get();
    186          if (!bs) {
    187            throw new Error("Could not get initialized BackupService.");
    188          }
    189 
    190          let [archivePath, recoveryCode, recoveryPath, outerResolve] = arguments;
    191          (async () => {
    192            let newProfileRootPath = await IOUtils.createUniqueDirectory(
    193              PathUtils.tempDir,
    194              "recoverFromBackupArchiveTest-newProfileRoot"
    195            );
    196 
    197            // This is some hackery to make it so that OSKeyStore doesn't kick
    198            // off an OS authentication dialog in our test, and also to make
    199            // sure we don't blow away the _real_ OSKeyStore key for the browser
    200            // on the system that this test is running on. Normally, I'd use
    201            // OSKeyStoreTestUtils.setup to do this, but apparently the
    202            // testing-common modules aren't available in Marionette tests.
    203            const ORIGINAL_STORE_LABEL = OSKeyStore.STORE_LABEL;
    204            OSKeyStore.STORE_LABEL = "test-" + Math.random().toString(36).substr(2);
    205 
    206            let newProfile = await bs.recoverFromBackupArchive(archivePath, recoveryCode, false, recoveryPath, newProfileRootPath);
    207 
    208            if (!newProfile) {
    209              throw new Error("Could not create recovery profile.");
    210            }
    211 
    212            let intermediateClientID = await ClientID.getClientID();
    213 
    214            return [newProfile.name, newProfile.rootDir.path, intermediateClientID, OSKeyStore.STORE_LABEL];
    215          })().then(outerResolve);
    216        """,
    217            script_args=[archivePath, recoveryCode, recoveryPath],
    218        )
    219 
    220        print(f"Recovery name: {newProfileName}")
    221        print(f"Recovery path: {newProfilePath}")
    222        print(f"Intermediate clientID: {intermediateClientID}")
    223        print(f"Persisting fake OSKeyStore label: {osKeyStoreLabel}")
    224 
    225        self.marionette.quit()
    226        originalProfile = self.marionette.instance.profile
    227        self.marionette.instance.profile = newProfilePath
    228        self.marionette.start_session()
    229        self.marionette.set_context("chrome")
    230 
    231        # Ensure that all postRecovery actions have completed, and that
    232        # encryption is enabled.
    233        encryptionEnabled = self.marionette.execute_async_script(
    234            """
    235          const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs");
    236          let bs = BackupService.get();
    237          if (!bs) {
    238            throw new Error("Could not get initialized BackupService.");
    239          }
    240 
    241          let [outerResolve] = arguments;
    242          (async () => {
    243            await bs.postRecoveryComplete;
    244 
    245            await bs.loadEncryptionState();
    246            return bs.state.encryptionEnabled;
    247          })().then(outerResolve);
    248        """
    249        )
    250        self.assertTrue(encryptionEnabled)
    251 
    252        self.verify_recovered_test_cookie()
    253        self.verify_recovered_test_login()
    254        self.verify_recovered_test_certificate()
    255        self.verify_recovered_saved_address()
    256        self.verify_recovered_identity_credential()
    257        self.verify_recovered_form_history()
    258        self.verify_recovered_asrouter_snippets_data()
    259        self.verify_recovered_protections_data()
    260        self.verify_recovered_bookmarks()
    261        self.verify_recovered_history()
    262        self.verify_recovered_preferences()
    263        self.verify_recovered_permissions()
    264        self.verify_recovered_payment_methods(osKeyStoreLabel)
    265        self.verify_recovered_sessionstore()
    266 
    267        # Clean up the temporary OSKeyStore label
    268        self.marionette.execute_async_script(
    269            """
    270          const { OSKeyStore } = ChromeUtils.importESModule("resource://gre/modules/OSKeyStore.sys.mjs");
    271          let [osKeyStoreLabel, outerResolve] = arguments;
    272 
    273          OSKeyStore.STORE_LABEL = osKeyStoreLabel;
    274 
    275          (async () => {
    276            await OSKeyStore.cleanup();
    277          })().then(outerResolve);
    278        """,
    279            script_args=[osKeyStoreLabel],
    280        )
    281 
    282        # Now also ensure that the recovered profile new client ID and not that
    283        # one from the intermediate profile that initiated recovery.
    284        recoveredClientID = self.marionette.execute_async_script(
    285            """
    286          const { ClientID } = ChromeUtils.importESModule("resource://gre/modules/ClientID.sys.mjs");
    287          let [outerResolve] = arguments;
    288          (async () => {
    289            return ClientID.getClientID();
    290          })().then(outerResolve);
    291        """
    292        )
    293        self.assertNotEqual(recoveredClientID, intermediateClientID)
    294 
    295        self.marionette.quit()
    296        self.marionette.instance.profile = originalProfile
    297        self.marionette.start_session()
    298        self.marionette.set_context("chrome")
    299 
    300        # Don't pollute the profile list by getting rid of the one we just created.
    301        self.marionette.execute_async_script(
    302            """
    303          let [newProfileName, outerResolve] = arguments;
    304          let profileSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
    305            Ci.nsIToolkitProfileService
    306          );
    307          let profile = profileSvc.getProfileByName(newProfileName);
    308          profile.remove(true);
    309          profileSvc.asyncFlush().then(outerResolve);
    310        """,
    311            script_args=[newProfileName],
    312        )
    313 
    314        # Cleanup the archive we moved, and the recovery folder we decompressed to.
    315        mozfile.remove(archivePath)
    316        mozfile.remove(recoveryPath)
    317 
    318    def test_backup_disablement_in_new_session(self):
    319        archiveDestPath = os.path.join(
    320            tempfile.gettempdir(), "backup-dest-disable-test"
    321        )
    322 
    323        [archivePath, lastBackupFileName] = self.marionette.execute_async_script(
    324            """
    325          const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs");
    326          let bs = BackupService.init();
    327          if (!bs) {
    328            throw new Error("Could not get initialized BackupService.");
    329          }
    330 
    331          let [archiveDestPath, outerResolve] = arguments;
    332          bs.setParentDirPath(archiveDestPath);
    333 
    334          (async () => {
    335            bs.setScheduledBackups(true);
    336            let { archivePath } = await bs.createBackup();
    337            if (!archivePath) {
    338              throw new Error("Could not create backup.");
    339            }
    340 
    341            let lastBackupFileName = Services.prefs.getStringPref("browser.backup.scheduled.last-backup-file", "");
    342            return [archivePath, lastBackupFileName];
    343          })().then(outerResolve);
    344        """,
    345            script_args=[archiveDestPath],
    346        )
    347 
    348        print(f"Created backup at: {archivePath}")
    349        print(f"Last backup filename: {lastBackupFileName}")
    350 
    351        self.marionette.quit()
    352        self.marionette.start_session()
    353        self.marionette.set_context("chrome")
    354 
    355        if os.path.exists(archivePath):
    356            print(f"File size: {os.path.getsize(archivePath)} bytes")
    357 
    358        self.marionette.execute_async_script(
    359            """
    360 
    361          ChromeUtils.defineESModuleGetters(this, {
    362            BackupService: "resource:///modules/backup/BackupService.sys.mjs",
    363            ASRouterTargeting: "resource:///modules/asrouter/ASRouterTargeting.sys.mjs",
    364          });
    365 
    366          let bs = BackupService.init();
    367          if (!bs) {
    368            throw new Error("Could not get initialized BackupService.");
    369          }
    370 
    371          let [outerResolve] = arguments;
    372          (async () => {
    373            await ASRouterTargeting.Environment.backupsInfo;
    374            await bs.cleanupBackupFiles();
    375            bs.setScheduledBackups(false);
    376          })().then(outerResolve);
    377        """,
    378            script_args=[],
    379        )
    380 
    381        archiveDeletedAfterDisable = not os.path.exists(archivePath)
    382        self.assertTrue(
    383            archiveDeletedAfterDisable,
    384            f"Backup file should be deleted after disabling backups. Path: {archivePath}, exists: {os.path.exists(archivePath)}",
    385        )
    386 
    387    def add_test_cookie(self):
    388        self.marionette.execute_async_script(
    389            """
    390          let [outerResolve] = arguments;
    391 
    392          (async () => {
    393            // We'll just add a single cookie, and then make sure that it shows
    394            // up on the other side.
    395            Services.cookies.removeAll();
    396            Services.cookies.add(
    397              ".example.com",
    398              "/",
    399              "first",
    400              "one",
    401              false,
    402              false,
    403              false,
    404              Date.now() + 1000,
    405              {},
    406              Ci.nsICookie.SAMESITE_UNSET,
    407              Ci.nsICookie.SCHEME_HTTP
    408            );
    409          })().then(outerResolve);
    410        """
    411        )
    412 
    413    def verify_recovered_test_cookie(self):
    414        cookiesLength = self.marionette.execute_async_script(
    415            """
    416          let [outerResolve] = arguments;
    417          (async () => {
    418            let cookies = Services.cookies.getCookiesFromHost("example.com", {});
    419            return cookies.length;
    420          })().then(outerResolve);
    421        """
    422        )
    423        # Expect cookies to be removed from the backup.
    424        self.assertEqual(cookiesLength, 0)
    425 
    426    def add_test_login(self):
    427        self.marionette.execute_async_script(
    428            """
    429          let [outerResolve] = arguments;
    430          (async () => {
    431            // Let's start with adding a single password
    432            Services.logins.removeAllLogins();
    433 
    434            const nsLoginInfo = new Components.Constructor(
    435              "@mozilla.org/login-manager/loginInfo;1",
    436              Ci.nsILoginInfo,
    437              "init"
    438            );
    439 
    440            const login1 = new nsLoginInfo(
    441              "https://example.com",
    442              "https://example.com",
    443              null,
    444              "notifyu1",
    445              "notifyp1",
    446              "user",
    447              "pass"
    448            );
    449            await Services.logins.addLoginAsync(login1);
    450          })().then(outerResolve);
    451        """
    452        )
    453 
    454    def verify_recovered_test_login(self):
    455        loginsLength = self.marionette.execute_async_script(
    456            """
    457          let [outerResolve] = arguments;
    458          (async () => {
    459            let logins = await Services.logins.searchLoginsAsync({
    460              origin: "https://example.com",
    461            });
    462            return logins.length;
    463          })().then(outerResolve);
    464        """
    465        )
    466        self.assertEqual(loginsLength, 1)
    467 
    468    def add_test_certificate(self):
    469        certPath = os.path.join(os.path.dirname(__file__), "http2-ca.pem")
    470        self.marionette.execute_async_script(
    471            """
    472          let [certPath, certDbKey, outerResolve] = arguments;
    473          (async () => {
    474            const { NetUtil } = ChromeUtils.importESModule(
    475              "resource://gre/modules/NetUtil.sys.mjs"
    476            );
    477 
    478            let certDb = Cc["@mozilla.org/security/x509certdb;1"].getService(
    479              Ci.nsIX509CertDB
    480            );
    481 
    482            if (certDb.findCertByDBKey(certDbKey)) {
    483              throw new Error("Should not have this certificate yet!");
    484            }
    485 
    486            let certFile = await IOUtils.getFile(certPath);
    487            let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
    488              Ci.nsIFileInputStream
    489            );
    490            fstream.init(certFile, -1, 0, 0);
    491            let data = NetUtil.readInputStreamToString(fstream, fstream.available());
    492            fstream.close();
    493 
    494            let pem = data.replace(/-----BEGIN CERTIFICATE-----/, "")
    495                          .replace(/-----END CERTIFICATE-----/, "")
    496                          .replace(/[\\r\\n]/g, "");
    497            let cert = certDb.addCertFromBase64(pem, "CTu,u,u");
    498 
    499            if (cert.dbKey != certDbKey) {
    500              throw new Error("The inserted certificate DB key is unexpected.");
    501            }
    502          })().then(outerResolve);
    503        """,
    504            script_args=[certPath, self._cert_db_key],
    505        )
    506 
    507    def verify_recovered_test_certificate(self):
    508        certExists = self.marionette.execute_async_script(
    509            """
    510          let [certDbKey, outerResolve] = arguments;
    511          (async () => {
    512            let certDb = Cc["@mozilla.org/security/x509certdb;1"].getService(
    513              Ci.nsIX509CertDB
    514            );
    515            return certDb.findCertByDBKey(certDbKey) != null;
    516          })().then(outerResolve);
    517        """,
    518            script_args=[self._cert_db_key],
    519        )
    520        self.assertTrue(certExists)
    521 
    522    def add_test_saved_address(self):
    523        self.marionette.execute_async_script(
    524            """
    525          const { formAutofillStorage } = ChromeUtils.importESModule(
    526            "resource://autofill/FormAutofillStorage.sys.mjs"
    527          );
    528 
    529          let [outerResolve] = arguments;
    530          (async () => {
    531            const TEST_ADDRESS_1 = {
    532              "given-name": "John",
    533              "additional-name": "R.",
    534              "family-name": "Smith",
    535              organization: "World Wide Web Consortium",
    536              "street-address": "32 Vassar Street\\\nMIT Room 32-G524",
    537              "address-level2": "Cambridge",
    538              "address-level1": "MA",
    539              "postal-code": "02139",
    540              country: "US",
    541              tel: "+15195555555",
    542              email: "user@example.com",
    543            };
    544            await formAutofillStorage.initialize();
    545            formAutofillStorage.addresses.removeAll();
    546            await formAutofillStorage.addresses.add(TEST_ADDRESS_1);
    547          })().then(outerResolve);
    548        """
    549        )
    550 
    551    def verify_recovered_saved_address(self):
    552        addressesLength = self.marionette.execute_async_script(
    553            """
    554          const { formAutofillStorage } = ChromeUtils.importESModule(
    555            "resource://autofill/FormAutofillStorage.sys.mjs"
    556          );
    557 
    558          let [outerResolve] = arguments;
    559          (async () => {
    560            await formAutofillStorage.initialize();
    561            let addresses = await formAutofillStorage.addresses.getAll();
    562            return addresses.length;
    563          })().then(outerResolve);
    564        """
    565        )
    566        self.assertEqual(addressesLength, 1)
    567 
    568    def add_test_identity_credential(self):
    569        self.marionette.execute_async_script(
    570            """
    571          let [outerResolve] = arguments;
    572          (async () => {
    573            let service = Cc["@mozilla.org/browser/identity-credential-storage-service;1"]
    574                            .getService(Ci.nsIIdentityCredentialStorageService);
    575            service.clear();
    576 
    577            let testPrincipal = Services.scriptSecurityManager.createContentPrincipal(
    578              Services.io.newURI("https://test.com/"),
    579              {}
    580            );
    581            let idpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
    582              Services.io.newURI("https://idp-test.com/"),
    583              {}
    584            );
    585 
    586            service.setState(
    587              testPrincipal,
    588              idpPrincipal,
    589              "ID",
    590              true,
    591              true
    592            );
    593 
    594          })().then(outerResolve);
    595        """
    596        )
    597 
    598    def verify_recovered_identity_credential(self):
    599        [registered, allowLogout] = self.marionette.execute_async_script(
    600            """
    601          let [outerResolve] = arguments;
    602          (async () => {
    603            let service = Cc["@mozilla.org/browser/identity-credential-storage-service;1"]
    604                            .getService(Ci.nsIIdentityCredentialStorageService);
    605 
    606            let testPrincipal = Services.scriptSecurityManager.createContentPrincipal(
    607              Services.io.newURI("https://test.com/"),
    608              {}
    609            );
    610            let idpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
    611              Services.io.newURI("https://idp-test.com/"),
    612              {}
    613            );
    614 
    615            let registered = {};
    616            let allowLogout = {};
    617 
    618            service.getState(
    619              testPrincipal,
    620              idpPrincipal,
    621              "ID",
    622              registered,
    623              allowLogout
    624            );
    625 
    626            return [registered.value, allowLogout.value];
    627          })().then(outerResolve);
    628        """
    629        )
    630        self.assertTrue(registered)
    631        self.assertTrue(allowLogout)
    632 
    633    def add_test_form_history(self):
    634        self.marionette.execute_async_script(
    635            """
    636          const { FormHistory } = ChromeUtils.importESModule(
    637            "resource://gre/modules/FormHistory.sys.mjs"
    638          );
    639 
    640          let [outerResolve] = arguments;
    641          (async () => {
    642            await FormHistory.update({
    643              op: "add",
    644              fieldname: "some-test-field",
    645              value: "I was recovered!",
    646              timesUsed: 1,
    647              firstUsed: 0,
    648              lastUsed: 0,
    649            });
    650 
    651          })().then(outerResolve);
    652        """
    653        )
    654 
    655    def verify_recovered_form_history(self):
    656        formHistoryResultsLength = self.marionette.execute_async_script(
    657            """
    658          const { FormHistory } = ChromeUtils.importESModule(
    659            "resource://gre/modules/FormHistory.sys.mjs"
    660          );
    661 
    662          let [outerResolve] = arguments;
    663          (async () => {
    664            let results = await FormHistory.search(
    665              ["guid"],
    666              { fieldname: "some-test-field" }
    667            );
    668            return results.length;
    669          })().then(outerResolve);
    670        """
    671        )
    672        self.assertEqual(formHistoryResultsLength, 1)
    673 
    674    def add_test_asrouter_snippets_data(self):
    675        self.marionette.execute_async_script(
    676            """
    677          const { ASRouterStorage } = ChromeUtils.importESModule(
    678            "resource:///modules/asrouter/ASRouterStorage.sys.mjs",
    679          );
    680          const SNIPPETS_TABLE_NAME = "snippets";
    681 
    682          let [outerResolve] = arguments;
    683          (async () => {
    684            let storage = new ASRouterStorage({
    685              storeNames: [SNIPPETS_TABLE_NAME],
    686            });
    687            let snippetsTable = await storage.getDbTable(SNIPPETS_TABLE_NAME);
    688            await snippetsTable.set("backup-test", "some-test-value");
    689          })().then(outerResolve);
    690        """
    691        )
    692 
    693    def verify_recovered_asrouter_snippets_data(self):
    694        snippetsResult = self.marionette.execute_async_script(
    695            """
    696          const { ASRouterStorage } = ChromeUtils.importESModule(
    697            "resource:///modules/asrouter/ASRouterStorage.sys.mjs",
    698          );
    699          const SNIPPETS_TABLE_NAME = "snippets";
    700 
    701          let [outerResolve] = arguments;
    702          (async () => {
    703            let storage = new ASRouterStorage({
    704              storeNames: [SNIPPETS_TABLE_NAME],
    705            });
    706            let snippetsTable = await storage.getDbTable(SNIPPETS_TABLE_NAME);
    707            return await snippetsTable.get("backup-test");
    708          })().then(outerResolve);
    709        """
    710        )
    711        self.assertEqual(snippetsResult, "some-test-value")
    712 
    713    def add_test_protections_data(self):
    714        self.marionette.execute_async_script(
    715            """
    716          const TrackingDBService = Cc["@mozilla.org/tracking-db-service;1"]
    717                                      .getService(Ci.nsITrackingDBService);
    718 
    719          let [outerResolve] = arguments;
    720          (async () => {
    721            let entry = {
    722              "https://test.com": [
    723                [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1],
    724              ],
    725            };
    726            await TrackingDBService.clearAll();
    727            await TrackingDBService.saveEvents(JSON.stringify(entry));
    728          })().then(outerResolve);
    729        """
    730        )
    731 
    732    def verify_recovered_protections_data(self):
    733        eventsSum = self.marionette.execute_async_script(
    734            """
    735          const TrackingDBService = Cc["@mozilla.org/tracking-db-service;1"]
    736                                      .getService(Ci.nsITrackingDBService);
    737 
    738          let [outerResolve] = arguments;
    739          (async () => {
    740            return TrackingDBService.sumAllEvents();
    741          })().then(outerResolve);
    742        """
    743        )
    744        self.assertEqual(eventsSum, 1)
    745 
    746    def add_test_bookmarks(self):
    747        self.marionette.execute_async_script(
    748            """
    749          const { PlacesUtils } = ChromeUtils.importESModule(
    750            "resource://gre/modules/PlacesUtils.sys.mjs"
    751          );
    752 
    753          let [outerResolve] = arguments;
    754          (async () => {
    755            await PlacesUtils.bookmarks.eraseEverything();
    756            await PlacesUtils.bookmarks.insert({
    757              parentGuid: PlacesUtils.bookmarks.toolbarGuid,
    758              title: "Some test page",
    759              url: Services.io.newURI("https://www.backup.test/"),
    760            });
    761          })().then(outerResolve);
    762        """
    763        )
    764 
    765    def verify_recovered_bookmarks(self):
    766        bookmarkExists = self.marionette.execute_async_script(
    767            """
    768          const { PlacesUtils } = ChromeUtils.importESModule(
    769            "resource://gre/modules/PlacesUtils.sys.mjs"
    770          );
    771 
    772          let [outerResolve] = arguments;
    773          (async () => {
    774            let url = Services.io.newURI("https://www.backup.test/");
    775            let bookmark = await PlacesUtils.bookmarks.fetch({ url });
    776            return bookmark != null;
    777          })().then(outerResolve);
    778        """
    779        )
    780        self.assertTrue(bookmarkExists)
    781 
    782    def add_test_history(self):
    783        self.marionette.execute_async_script(
    784            """
    785          const { PlacesUtils } = ChromeUtils.importESModule(
    786            "resource://gre/modules/PlacesUtils.sys.mjs"
    787          );
    788 
    789          let [outerResolve] = arguments;
    790          (async () => {
    791            await PlacesUtils.history.clear();
    792 
    793            let entry = {
    794              url: "http://my-restored-history.com",
    795              visits: [{ transition: PlacesUtils.history.TRANSITION_LINK }],
    796            };
    797 
    798            await PlacesUtils.history.insertMany([entry]);
    799          })().then(outerResolve);
    800        """
    801        )
    802 
    803    def verify_recovered_history(self):
    804        historyExists = self.marionette.execute_async_script(
    805            """
    806          const { PlacesUtils } = ChromeUtils.importESModule(
    807            "resource://gre/modules/PlacesUtils.sys.mjs"
    808          );
    809 
    810          let [outerResolve] = arguments;
    811          (async () => {
    812            let entry = await PlacesUtils.history.fetch("http://my-restored-history.com");
    813            return entry != null;
    814          })().then(outerResolve);
    815        """
    816        )
    817        self.assertTrue(historyExists)
    818 
    819    def add_test_preferences(self):
    820        self.marionette.execute_script(
    821            """
    822          Services.prefs.setBoolPref("test-pref-for-backup", true)
    823        """
    824        )
    825 
    826    def verify_recovered_preferences(self):
    827        prefExists = self.marionette.execute_script(
    828            """
    829          return Services.prefs.getBoolPref("test-pref-for-backup", false);
    830        """
    831        )
    832        self.assertTrue(prefExists)
    833 
    834    def add_test_permissions(self):
    835        self.marionette.execute_script(
    836            """
    837          let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
    838            "https://test-permission-site.com"
    839          );
    840          Services.perms.addFromPrincipal(
    841            principal,
    842            "desktop-notification",
    843            Services.perms.ALLOW_ACTION
    844          );
    845        """
    846        )
    847 
    848    def verify_recovered_permissions(self):
    849        permissionExists = self.marionette.execute_script(
    850            """
    851          let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
    852            "https://test-permission-site.com"
    853          );
    854          let perms = Services.perms.getAllForPrincipal(principal);
    855          if (perms.length != 1) {
    856            throw new Error("Got an unexpected number of permissions");
    857          }
    858          return perms[0].type == "desktop-notification"
    859        """
    860        )
    861        self.assertTrue(permissionExists)
    862 
    863    def add_test_payment_methods(self):
    864        self.marionette.execute_async_script(
    865            """
    866          const { formAutofillStorage } = ChromeUtils.importESModule(
    867            "resource://autofill/FormAutofillStorage.sys.mjs"
    868          );
    869 
    870          let [outerResolve] = arguments;
    871          (async () => {
    872            await formAutofillStorage.initialize();
    873            await formAutofillStorage.creditCards.add({
    874              "cc-name": "Foxy the Firefox",
    875              "cc-number": "5555555555554444",
    876              "cc-exp-month": 5,
    877              "cc-exp-year": 2099,
    878            });
    879          })().then(outerResolve);
    880        """
    881        )
    882 
    883    def verify_recovered_payment_methods(self, osKeyStoreLabel):
    884        cardExists = self.marionette.execute_async_script(
    885            """
    886          const { formAutofillStorage } = ChromeUtils.importESModule(
    887            "resource://autofill/FormAutofillStorage.sys.mjs"
    888          );
    889          let nativeOSKeyStore = Cc["@mozilla.org/security/oskeystore;1"].getService(
    890            Ci.nsIOSKeyStore
    891          );
    892 
    893          let [osKeyStoreLabel, outerResolve] = arguments;
    894 
    895          (async () => {
    896            await formAutofillStorage.initialize();
    897            let cards = await formAutofillStorage.creditCards.getAll();
    898 
    899            if (cards.length != 1) {
    900              return false;
    901            }
    902            let card = cards[0];
    903            if (card["cc-name"] != "Foxy the Firefox") {
    904              return false;
    905            }
    906 
    907            if (card["cc-exp-month"] != "5") {
    908              return false;
    909            }
    910 
    911            if (card["cc-exp-year"] != "2099") {
    912              return false;
    913            }
    914 
    915            if (!card["cc-number-encrypted"]) {
    916              return false;
    917            }
    918 
    919            // Hack around OSKeyStore's insistence on asking for
    920            // reauthentication by using the underlying nativeOSKeyStore
    921            // to decrypt the credit card number to check it.
    922            let plaintextCardBytes =
    923              await nativeOSKeyStore.asyncDecryptBytes(
    924                osKeyStoreLabel,
    925                card["cc-number-encrypted"]
    926              );
    927            let plaintextCard = String.fromCharCode.apply(
    928              String,
    929              plaintextCardBytes
    930            );
    931            if (plaintextCard != "5555555555554444") {
    932              return false;
    933            }
    934 
    935            return true;
    936          })().then(outerResolve);
    937        """,
    938            script_args=[osKeyStoreLabel],
    939        )
    940        self.assertTrue(cardExists)
    941 
    942    def add_test_sessionstore(self):
    943        with self.marionette.using_context("content"):
    944            self.marionette.navigate("about:mozilla")
    945 
    946    def verify_recovered_sessionstore(self):
    947        [tabCount, url] = self.marionette.execute_script(
    948            """
    949          const { SessionStore } = ChromeUtils.importESModule(
    950            "resource:///modules/sessionstore/SessionStore.sys.mjs"
    951          );
    952          const session = SessionStore.getCurrentState(true);
    953          const win = session.windows[0];
    954          const tabLen = win.tabs.length;
    955          const tab = win.tabs[0];
    956          const entry = tab.entries[0];
    957          const url = entry.url;
    958          return [tabLen, url];
    959        """
    960        )
    961 
    962        self.assertEqual(tabCount, 1)
    963        self.assertEqual(url, "about:mozilla")