tor-browser

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

test_BackupService.js (41562B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 https://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const { BasePromiseWorker } = ChromeUtils.importESModule(
      7  "resource://gre/modules/PromiseWorker.sys.mjs"
      8 );
      9 const { JsonSchema } = ChromeUtils.importESModule(
     10  "resource://gre/modules/JsonSchema.sys.mjs"
     11 );
     12 const { UIState } = ChromeUtils.importESModule(
     13  "resource://services-sync/UIState.sys.mjs"
     14 );
     15 const { ClientID } = ChromeUtils.importESModule(
     16  "resource://gre/modules/ClientID.sys.mjs"
     17 );
     18 const { ERRORS } = ChromeUtils.importESModule(
     19  "chrome://browser/content/backup/backup-constants.mjs"
     20 );
     21 
     22 const { TestUtils } = ChromeUtils.importESModule(
     23  "resource://testing-common/TestUtils.sys.mjs"
     24 );
     25 
     26 const LAST_BACKUP_TIMESTAMP_PREF_NAME =
     27  "browser.backup.scheduled.last-backup-timestamp";
     28 const LAST_BACKUP_FILE_NAME_PREF_NAME =
     29  "browser.backup.scheduled.last-backup-file";
     30 const BACKUP_ARCHIVE_ENABLED_PREF_NAME = "browser.backup.archive.enabled";
     31 const BACKUP_RESTORE_ENABLED_PREF_NAME = "browser.backup.restore.enabled";
     32 
     33 /** @type {nsIToolkitProfile} */
     34 let currentProfile;
     35 
     36 // Mock backup metadata
     37 const DATE = "2024-06-25T21:59:11.777Z";
     38 const IS_ENCRYPTED = true;
     39 const DEVICE_NAME = "test-device";
     40 const APP_NAME = "test-app-name";
     41 const APP_VERSION = "test-app-version";
     42 const BUILD_ID = "test-build-id";
     43 const OS_NAME = "test-os-name";
     44 const OS_VERSION = "test-os-version";
     45 const TELEMETRY_ENABLED = true;
     46 const LEGACY_CLIENT_ID = "legacy-client-id";
     47 
     48 add_setup(function () {
     49  currentProfile = setupProfile();
     50 });
     51 
     52 /**
     53 * A utility function for testing BackupService.createBackup. This helper
     54 * function:
     55 *
     56 * 1. Produces a backup of fake resources
     57 * 2. Recovers the backup into a new profile directory
     58 * 3. Ensures that the resources had their backup/recovery methods called
     59 *
     60 * @param {object} sandbox
     61 *   The Sinon sandbox to be used stubs and mocks. The test using this helper
     62 *   is responsible for creating and resetting this sandbox.
     63 * @param {function(BackupService, BackupManifest): void} taskFn
     64 *   A function that is run once all default checks are done.
     65 *   After this function returns, all resources will be cleaned up.
     66 * @returns {Promise<undefined>}
     67 */
     68 async function testCreateBackupHelper(sandbox, taskFn) {
     69  Services.telemetry.clearEvents();
     70  Services.fog.testResetFOG();
     71 
     72  // Handle for the metric for total byte size of staging folder
     73  let totalBackupSizeHistogram = TelemetryTestUtils.getAndClearHistogram(
     74    "BROWSER_BACKUP_TOTAL_BACKUP_SIZE"
     75  );
     76  // Handle for the metric for total byte size of single-file archive
     77  let compressedArchiveSizeHistogram = TelemetryTestUtils.getAndClearHistogram(
     78    "BROWSER_BACKUP_COMPRESSED_ARCHIVE_SIZE"
     79  );
     80  // Handle for the metric for total time taking by profile backup
     81  let backupTimerHistogram = TelemetryTestUtils.getAndClearHistogram(
     82    "BROWSER_BACKUP_TOTAL_BACKUP_TIME_MS"
     83  );
     84 
     85  const EXPECTED_CLIENT_ID = await ClientID.getClientID();
     86  const EXPECTED_PROFILE_GROUP_ID = await ClientID.getProfileGroupID();
     87 
     88  // Enable the scheduled backups pref so that backups can be deleted. We're
     89  // not calling initBackupScheduler on the BackupService that we're
     90  // constructing, so there's no danger of accidentally having a backup be
     91  // created during this test if there's an idle period.
     92  Services.prefs.setBoolPref("browser.backup.scheduled.enabled", true);
     93  registerCleanupFunction(() => {
     94    Services.prefs.clearUserPref("browser.backup.scheduled.enabled");
     95  });
     96 
     97  let fake1ManifestEntry = { fake1: "hello from 1" };
     98  sandbox
     99    .stub(FakeBackupResource1.prototype, "backup")
    100    .resolves(fake1ManifestEntry);
    101  sandbox.stub(FakeBackupResource1.prototype, "recover").resolves();
    102 
    103  sandbox
    104    .stub(FakeBackupResource2.prototype, "backup")
    105    .rejects(new Error("Some failure to backup"));
    106  sandbox.stub(FakeBackupResource2.prototype, "recover");
    107 
    108  let fake3ManifestEntry = { fake3: "hello from 3" };
    109  let fake3PostRecoveryEntry = { someData: "hello again from 3" };
    110  sandbox
    111    .stub(FakeBackupResource3.prototype, "backup")
    112    .resolves(fake3ManifestEntry);
    113  sandbox
    114    .stub(FakeBackupResource3.prototype, "recover")
    115    .resolves(fake3PostRecoveryEntry);
    116 
    117  let bs = BackupService.init({
    118    FakeBackupResource1,
    119    FakeBackupResource2,
    120    FakeBackupResource3,
    121  });
    122 
    123  let fakeProfilePath = await IOUtils.createUniqueDirectory(
    124    PathUtils.tempDir,
    125    "createBackupTest"
    126  );
    127 
    128  Assert.ok(!bs.state.lastBackupDate, "No backup date is stored in state.");
    129  let { manifest, archivePath: backupFilePath } = await bs.createBackup({
    130    profilePath: fakeProfilePath,
    131  });
    132  Assert.notStrictEqual(
    133    bs.state.lastBackupDate,
    134    null,
    135    "The backup date was recorded."
    136  );
    137 
    138  let legacyEvents = TelemetryTestUtils.getEvents(
    139    { category: "browser.backup", method: "created", object: "BackupService" },
    140    { process: "parent" }
    141  );
    142  Assert.equal(legacyEvents.length, 1, "Found the created legacy event.");
    143  let events = Glean.browserBackup.created.testGetValue();
    144  Assert.equal(events.length, 1, "Found the created Glean event.");
    145 
    146  // Validate total backup time metrics were recorded
    147  assertSingleTimeMeasurement(
    148    Glean.browserBackup.totalBackupTime.testGetValue()
    149  );
    150  assertHistogramMeasurementQuantity(
    151    backupTimerHistogram,
    152    1,
    153    "Should have collected a single measurement for total backup time"
    154  );
    155 
    156  Assert.ok(await IOUtils.exists(backupFilePath), "The backup file exists");
    157 
    158  let archiveDateSuffix = bs.generateArchiveDateSuffix(
    159    new Date(manifest.meta.date)
    160  );
    161 
    162  // We also expect the HTML file to have been written to the folder pointed
    163  // at by browser.backups.location, within backupDirPath folder.
    164  const EXPECTED_ARCHIVE_PATH = PathUtils.join(
    165    bs.state.backupDirPath,
    166    `${BackupService.BACKUP_FILE_NAME}_${manifest.meta.profileName}_${archiveDateSuffix}.html`
    167  );
    168  Assert.ok(
    169    await IOUtils.exists(EXPECTED_ARCHIVE_PATH),
    170    "Single-file backup archive was written."
    171  );
    172  Assert.equal(
    173    backupFilePath,
    174    EXPECTED_ARCHIVE_PATH,
    175    "Backup was written to the configured destination folder"
    176  );
    177 
    178  let snapshotsDirectoryPath = PathUtils.join(
    179    fakeProfilePath,
    180    BackupService.PROFILE_FOLDER_NAME,
    181    BackupService.SNAPSHOTS_FOLDER_NAME
    182  );
    183  let snapshotsDirectoryContentsPaths = await IOUtils.getChildren(
    184    snapshotsDirectoryPath
    185  );
    186  let snapshotsDirectoryContents = await Promise.all(
    187    snapshotsDirectoryContentsPaths.map(IOUtils.stat)
    188  );
    189  let snapshotsDirectorySubdirectories = snapshotsDirectoryContents.filter(
    190    file => file.type === "directory"
    191  );
    192  Assert.equal(
    193    snapshotsDirectorySubdirectories.length,
    194    0,
    195    "Snapshots directory should have had all staging folders cleaned up"
    196  );
    197 
    198  // 1 mebibyte minimum recorded value if total data size is under 1 mebibyte
    199  // This assumes that these BackupService tests do not create sizable fake files
    200  const SMALLEST_BACKUP_SIZE_BYTES = 1048576;
    201  const SMALLEST_BACKUP_SIZE_MEBIBYTES = 1;
    202 
    203  // Validate total (uncompressed profile data) size
    204  let totalBackupSize = Glean.browserBackup.totalBackupSize.testGetValue();
    205  Assert.equal(
    206    totalBackupSize.count,
    207    1,
    208    "Should have collected a single measurement for the total backup size"
    209  );
    210  Assert.equal(
    211    totalBackupSize.sum,
    212    SMALLEST_BACKUP_SIZE_BYTES,
    213    "Should have collected the right value for the total backup size"
    214  );
    215  TelemetryTestUtils.assertHistogram(
    216    totalBackupSizeHistogram,
    217    SMALLEST_BACKUP_SIZE_MEBIBYTES,
    218    1
    219  );
    220 
    221  // Validate final archive (compressed/encrypted profile data + HTML) size
    222  let compressedArchiveSize =
    223    Glean.browserBackup.compressedArchiveSize.testGetValue();
    224  Assert.equal(
    225    compressedArchiveSize.count,
    226    1,
    227    "Should have collected a single measurement for the backup compressed archive size"
    228  );
    229  Assert.equal(
    230    compressedArchiveSize.sum,
    231    SMALLEST_BACKUP_SIZE_BYTES,
    232    "Should have collected the right value for the backup compressed archive size"
    233  );
    234  TelemetryTestUtils.assertHistogram(
    235    compressedArchiveSizeHistogram,
    236    SMALLEST_BACKUP_SIZE_MEBIBYTES,
    237    1
    238  );
    239 
    240  // Check that resources were called from highest to lowest backup priority.
    241  sinon.assert.callOrder(
    242    FakeBackupResource3.prototype.backup,
    243    FakeBackupResource2.prototype.backup,
    244    FakeBackupResource1.prototype.backup
    245  );
    246 
    247  let schema = await BackupService.MANIFEST_SCHEMA;
    248  let validationResult = JsonSchema.validate(manifest, schema);
    249  Assert.ok(validationResult.valid, "Schema matches manifest");
    250  Assert.deepEqual(
    251    Object.keys(manifest.resources).sort(),
    252    ["fake1", "fake3"],
    253    "Manifest contains all expected BackupResource keys"
    254  );
    255  Assert.deepEqual(
    256    manifest.resources.fake1,
    257    fake1ManifestEntry,
    258    "Manifest contains the expected entry for FakeBackupResource1"
    259  );
    260  Assert.deepEqual(
    261    manifest.resources.fake3,
    262    fake3ManifestEntry,
    263    "Manifest contains the expected entry for FakeBackupResource3"
    264  );
    265  Assert.equal(
    266    manifest.meta.legacyClientID,
    267    EXPECTED_CLIENT_ID,
    268    "The client ID was stored properly."
    269  );
    270  Assert.equal(
    271    manifest.meta.profileGroupID,
    272    EXPECTED_PROFILE_GROUP_ID,
    273    "The profile group ID was stored properly."
    274  );
    275  Assert.equal(
    276    manifest.meta.profileName,
    277    currentProfile.name,
    278    "The profile name was stored properly"
    279  );
    280 
    281  let recoveredProfilePath = await IOUtils.createUniqueDirectory(
    282    PathUtils.tempDir,
    283    "createBackupTestRecoveredProfile"
    284  );
    285 
    286  let originalProfileName = currentProfile.name;
    287 
    288  let profileSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
    289    Ci.nsIToolkitProfileService
    290  );
    291  // make our current profile default
    292  profileSvc.defaultProfile = currentProfile;
    293 
    294  await bs.getBackupFileInfo(backupFilePath);
    295  const restoreID = bs.state.restoreID;
    296 
    297  // Intercept the telemetry that we want to check for before it gets submitted
    298  // and cleared out.
    299  let restoreStartedEvents;
    300  let restoreCompleteEvents;
    301  let restoreCompleteCallback = () => {
    302    Services.obs.removeObserver(
    303      restoreCompleteCallback,
    304      "browser-backup-restore-complete"
    305    );
    306    restoreStartedEvents = Glean.browserBackup.restoreStarted.testGetValue();
    307    restoreCompleteEvents = Glean.browserBackup.restoreComplete.testGetValue();
    308  };
    309  Services.obs.addObserver(
    310    restoreCompleteCallback,
    311    "browser-backup-restore-complete"
    312  );
    313 
    314  let recoveredProfile = await bs.recoverFromBackupArchive(
    315    backupFilePath,
    316    null,
    317    false,
    318    fakeProfilePath,
    319    recoveredProfilePath
    320  );
    321 
    322  Assert.ok(
    323    recoveredProfile.name.startsWith(originalProfileName),
    324    "Should maintain profile name across backup and restore"
    325  );
    326 
    327  Assert.ok(
    328    currentProfile.name.startsWith("old-"),
    329    "The old profile should be prefixed with old-"
    330  );
    331 
    332  Assert.strictEqual(
    333    profileSvc.defaultProfile,
    334    recoveredProfile,
    335    "The new profile should now be the default"
    336  );
    337 
    338  Assert.equal(
    339    restoreStartedEvents.length,
    340    1,
    341    "Should be a single restore start event after we start restoring a profile"
    342  );
    343  Assert.deepEqual(
    344    restoreStartedEvents[0].extra,
    345    { restore_id: restoreID },
    346    "Restore start event should have the right data"
    347  );
    348 
    349  Assert.equal(
    350    restoreCompleteEvents.length,
    351    1,
    352    "Should be a single restore complete event after we start restoring a profile"
    353  );
    354  Assert.deepEqual(
    355    restoreCompleteEvents[0].extra,
    356    { restore_id: restoreID },
    357    "Restore complete event should have the right data"
    358  );
    359 
    360  // Check that resources were recovered from highest to lowest backup priority.
    361  sinon.assert.callOrder(
    362    FakeBackupResource3.prototype.recover,
    363    FakeBackupResource1.prototype.recover
    364  );
    365 
    366  let postRecoveryFilePath = PathUtils.join(
    367    recoveredProfilePath,
    368    BackupService.POST_RECOVERY_FILE_NAME
    369  );
    370  Assert.ok(
    371    await IOUtils.exists(postRecoveryFilePath),
    372    "Should have created post-recovery data file"
    373  );
    374  let postRecoveryData = await IOUtils.readJSON(postRecoveryFilePath);
    375  Assert.deepEqual(
    376    postRecoveryData.fake3,
    377    fake3PostRecoveryEntry,
    378    "Should have post-recovery data from fake backup 3"
    379  );
    380 
    381  await Assert.rejects(
    382    IOUtils.readJSON(
    383      PathUtils.join(recoveredProfilePath, "datareporting", "state.json")
    384    ),
    385    /file does not exist/,
    386    "The telemetry state was cleared."
    387  );
    388 
    389  await taskFn(bs, manifest);
    390 
    391  await maybeRemovePath(backupFilePath);
    392  await maybeRemovePath(fakeProfilePath);
    393  await maybeRemovePath(recoveredProfilePath);
    394  await maybeRemovePath(EXPECTED_ARCHIVE_PATH);
    395 
    396  Services.prefs.clearUserPref(LAST_BACKUP_FILE_NAME_PREF_NAME);
    397 
    398  BackupService.uninit();
    399 }
    400 
    401 /**
    402 * A utility function for testing BackupService.deleteLastBackup. This helper
    403 * function:
    404 *
    405 * 1. Clears any pre-existing cached preference values for the last backup
    406 *    date and file name.
    407 * 2. Uses testCreateBackupHelper to create a backup file.
    408 * 3. Ensures that the state has been updated to reflect the created backup,
    409 *    and that the backup date and file name are cached to preferences.
    410 * 4. Runs an optional async taskFn
    411 * 5. Calls deleteLastBackup on the testCreateBackupHelper BackupService
    412 *    instance.
    413 * 6. Checks that the BackupService state for the last backup date and file name
    414 *    have been cleared, and that the preferences caches of those values have
    415 *    also been cleared.
    416 *
    417 * @param {function(string): Promise<void>|null} taskFn
    418 *   An optional function that is run after we've created a backup, but just
    419 *   before calling deleteLastBackup(). It is passed the path to the created
    420 *   backup file.
    421 * @returns {Promise<undefined>}
    422 */
    423 async function testDeleteLastBackupHelper(taskFn) {
    424  let sandbox = sinon.createSandbox();
    425 
    426  // Clear any last backup filenames and timestamps that might be lingering
    427  // from prior tests.
    428  Services.prefs.clearUserPref(LAST_BACKUP_TIMESTAMP_PREF_NAME);
    429  Services.prefs.clearUserPref(LAST_BACKUP_FILE_NAME_PREF_NAME);
    430 
    431  await testCreateBackupHelper(sandbox, async (bs, _manifest) => {
    432    Assert.notStrictEqual(
    433      bs.state.lastBackupDate,
    434      null,
    435      "Should have a last backup date recorded."
    436    );
    437    Assert.ok(
    438      bs.state.lastBackupFileName,
    439      "Should have a last backup file name recorded."
    440    );
    441    Assert.ok(
    442      Services.prefs.prefHasUserValue(LAST_BACKUP_TIMESTAMP_PREF_NAME),
    443      "Last backup date was cached in preferences."
    444    );
    445    Assert.ok(
    446      Services.prefs.prefHasUserValue(LAST_BACKUP_FILE_NAME_PREF_NAME),
    447      "Last backup file name was cached in preferences."
    448    );
    449    const LAST_BACKUP_FILE_PATH = PathUtils.join(
    450      bs.state.backupDirPath,
    451      bs.state.lastBackupFileName
    452    );
    453    Assert.ok(
    454      await IOUtils.exists(LAST_BACKUP_FILE_PATH),
    455      "The backup file was created and is still on the disk."
    456    );
    457 
    458    if (taskFn) {
    459      await taskFn(LAST_BACKUP_FILE_PATH);
    460    }
    461 
    462    // NB: On Windows, deletes of backups in tests run into an issue where
    463    // the file is locked briefly by the system and deletes fail with
    464    // NS_ERROR_FILE_IS_LOCKED.  See doFileRemovalOperation for details.
    465    // We therefore retry this delete a few times before accepting failure.
    466    await doFileRemovalOperation(async () => await bs.deleteLastBackup());
    467 
    468    Assert.equal(
    469      bs.state.lastBackupDate,
    470      null,
    471      "Should have cleared the last backup date"
    472    );
    473    Assert.equal(
    474      bs.state.lastBackupFileName,
    475      "",
    476      "Should have cleared the last backup file name"
    477    );
    478    Assert.ok(
    479      !Services.prefs.prefHasUserValue(LAST_BACKUP_TIMESTAMP_PREF_NAME),
    480      "Last backup date was cleared in preferences."
    481    );
    482    Assert.ok(
    483      !Services.prefs.prefHasUserValue(LAST_BACKUP_FILE_NAME_PREF_NAME),
    484      "Last backup file name was cleared in preferences."
    485    );
    486    Assert.ok(
    487      !(await IOUtils.exists(LAST_BACKUP_FILE_PATH)),
    488      "The backup file was deleted."
    489    );
    490  });
    491 
    492  sandbox.restore();
    493 }
    494 
    495 /**
    496 * Tests that calling BackupService.createBackup will call backup on each
    497 * registered BackupResource, and that each BackupResource will have a folder
    498 * created for them to write into. Tests in the signed-out state.
    499 */
    500 add_task(async function test_createBackup_signed_out() {
    501  let sandbox = sinon.createSandbox();
    502 
    503  sandbox
    504    .stub(UIState, "get")
    505    .returns({ status: UIState.STATUS_NOT_CONFIGURED });
    506  await testCreateBackupHelper(sandbox, (_bs, manifest) => {
    507    Assert.equal(
    508      manifest.meta.accountID,
    509      undefined,
    510      "Account ID should be undefined."
    511    );
    512    Assert.equal(
    513      manifest.meta.accountEmail,
    514      undefined,
    515      "Account email should be undefined."
    516    );
    517  });
    518 
    519  sandbox.restore();
    520 });
    521 
    522 /**
    523 * Tests that calling BackupService.createBackup will call backup on each
    524 * registered BackupResource, and that each BackupResource will have a folder
    525 * created for them to write into. Tests in the signed-in state.
    526 */
    527 add_task(async function test_createBackup_signed_in() {
    528  let sandbox = sinon.createSandbox();
    529 
    530  const TEST_UID = "ThisIsMyTestUID";
    531  const TEST_EMAIL = "foxy@mozilla.org";
    532 
    533  sandbox.stub(UIState, "get").returns({
    534    status: UIState.STATUS_SIGNED_IN,
    535    uid: TEST_UID,
    536    email: TEST_EMAIL,
    537  });
    538 
    539  await testCreateBackupHelper(sandbox, (_bs, manifest) => {
    540    Assert.equal(
    541      manifest.meta.accountID,
    542      TEST_UID,
    543      "Account ID should be set properly."
    544    );
    545    Assert.equal(
    546      manifest.meta.accountEmail,
    547      TEST_EMAIL,
    548      "Account email should be set properly."
    549    );
    550  });
    551 
    552  sandbox.restore();
    553 });
    554 
    555 /**
    556 * Makes a folder readonly.  Windows does not support read-only folders, so
    557 * this creates a file inside the folder and makes that read-only.
    558 *
    559 * @param {string}  folderpath Full path to folder to make read-only.
    560 * @param {boolean} isReadonly Whether to set or clear read-only status.
    561 */
    562 async function makeFolderReadonly(folderpath, isReadonly) {
    563  if (AppConstants.platform !== "win") {
    564    await IOUtils.setPermissions(folderpath, isReadonly ? 0o444 : 0o666);
    565    let folder = await IOUtils.getFile(folderpath);
    566    Assert.equal(
    567      folder.isWritable(),
    568      !isReadonly,
    569      `folder is ${isReadonly ? "" : "not "}read-only`
    570    );
    571  } else if (isReadonly) {
    572    // Permissions flags like 0o444 are not usually respected on Windows but in
    573    // the case of creating a unique file, the read-only status is.  See
    574    // OpenFile in nsLocalFileWin.cpp.
    575    let tempFilename = await IOUtils.createUniqueFile(
    576      folderpath,
    577      "readonlyfile",
    578      0o444
    579    );
    580    let file = await IOUtils.getFile(tempFilename);
    581    Assert.equal(file.isWritable(), false, "file in folder is read-only");
    582  } else {
    583    // Recursively set any folder contents to be writeable.
    584    let attrs = await IOUtils.getWindowsAttributes(folderpath);
    585    attrs.readonly = false;
    586    await IOUtils.setWindowsAttributes(folderpath, attrs, true /* recursive */);
    587  }
    588 }
    589 
    590 /**
    591 * Tests that read-only files in BackupService.createBackup cause backup
    592 * failure (createBackup returns null) and does not bubble up any errors.
    593 */
    594 add_task(
    595  {
    596    // We override read-only on Windows -- see
    597    // test_createBackup_override_readonly below.
    598    skip_if: () => AppConstants.platform == "win",
    599  },
    600  async function test_createBackup_robustToFileSystemErrors() {
    601    let sandbox = sinon.createSandbox();
    602    Services.fog.testResetFOG();
    603    // Handle for the metric for total time taking by profile backup
    604    let backupTimerHistogram = TelemetryTestUtils.getAndClearHistogram(
    605      "BROWSER_BACKUP_TOTAL_BACKUP_TIME_MS"
    606    );
    607 
    608    const TEST_UID = "ThisIsMyTestUID";
    609    const TEST_EMAIL = "foxy@mozilla.org";
    610 
    611    sandbox.stub(UIState, "get").returns({
    612      status: UIState.STATUS_SIGNED_IN,
    613      uid: TEST_UID,
    614      email: TEST_EMAIL,
    615    });
    616 
    617    // Create a read-only fake profile folder to which the backup service
    618    // won't be able to make writes
    619    let inaccessibleProfilePath = await IOUtils.createUniqueDirectory(
    620      PathUtils.tempDir,
    621      "createBackupErrorReadonly"
    622    );
    623    await makeFolderReadonly(inaccessibleProfilePath, true);
    624 
    625    const bs = new BackupService({});
    626 
    627    await bs
    628      .createBackup({ profilePath: inaccessibleProfilePath })
    629      .then(result => {
    630        Assert.equal(result, null, "Should return null on error");
    631 
    632        // Validate total backup time metrics were recorded
    633        const totalBackupTime =
    634          Glean.browserBackup.totalBackupTime.testGetValue();
    635        Assert.equal(
    636          totalBackupTime,
    637          null,
    638          "Should not have measured total backup time for failed backup"
    639        );
    640        assertHistogramMeasurementQuantity(backupTimerHistogram, 0);
    641      })
    642      .catch(() => {
    643        // Failure bubbles up an error for handling by the caller
    644      })
    645      .finally(async () => {
    646        await makeFolderReadonly(inaccessibleProfilePath, false);
    647        await IOUtils.remove(inaccessibleProfilePath, { recursive: true });
    648        sandbox.restore();
    649      });
    650  }
    651 );
    652 
    653 /**
    654 * Tests that BackupService.createBackup can override simple read-only status
    655 * when handling staging files.  That currently only works on Windows.
    656 */
    657 add_task(
    658  {
    659    skip_if: () => AppConstants.platform !== "win",
    660  },
    661  async function test_createBackup_override_readonly() {
    662    let sandbox = sinon.createSandbox();
    663 
    664    const TEST_UID = "ThisIsMyTestUID";
    665    const TEST_EMAIL = "foxy@mozilla.org";
    666 
    667    sandbox.stub(UIState, "get").returns({
    668      status: UIState.STATUS_SIGNED_IN,
    669      uid: TEST_UID,
    670      email: TEST_EMAIL,
    671    });
    672 
    673    // Create a fake profile folder that contains a read-only file.  We do this
    674    // because Windows does not respect read-only status on folders. The
    675    // file's read-only status will make the folder un(re)movable.
    676    let inaccessibleProfilePath = await IOUtils.createUniqueDirectory(
    677      PathUtils.tempDir,
    678      "createBackupErrorReadonly"
    679    );
    680    await makeFolderReadonly(inaccessibleProfilePath, true);
    681    await Assert.rejects(
    682      IOUtils.remove(inaccessibleProfilePath),
    683      /Could not remove/,
    684      "folder is not removable"
    685    );
    686 
    687    const bs = new BackupService({});
    688 
    689    await bs
    690      .createBackup({ profilePath: inaccessibleProfilePath })
    691      .then(result => {
    692        Assert.notEqual(result, null, "Should not return null on success");
    693      })
    694      .catch(e => {
    695        console.error(e);
    696        Assert.ok(false, "Should not have bubbled up an error");
    697      })
    698      .finally(async () => {
    699        await makeFolderReadonly(inaccessibleProfilePath, false);
    700        await IOUtils.remove(inaccessibleProfilePath, { recursive: true });
    701        await bs.deleteLastBackup();
    702        sandbox.restore();
    703      });
    704  }
    705 );
    706 
    707 /**
    708 * Creates a unique file in the given folder and tells a worker to keep it
    709 * open until we post a close message.  Checks that the folder is not
    710 * removable as a result.
    711 *
    712 * @param {string} folderpath
    713 * @returns {object} {{ path: string, worker: OpenFileWorker }}
    714 */
    715 async function openUniqueFileInFolder(folderpath) {
    716  let testFile = await IOUtils.createUniqueFile(folderpath, "openfile");
    717  await IOUtils.writeUTF8(testFile, "");
    718  Assert.ok(
    719    await IOUtils.exists(testFile),
    720    testFile + " should have been created"
    721  );
    722  // Use a worker to keep the testFile open.
    723  const worker = new BasePromiseWorker(
    724    "resource://test/data/test_keep_file_open.worker.js"
    725  );
    726  await worker.post("open", [testFile]);
    727 
    728  await Assert.rejects(
    729    IOUtils.remove(folderpath),
    730    /NS_ERROR_FILE_DIR_NOT_EMPTY/,
    731    "attempt to remove folder threw an exception"
    732  );
    733  Assert.ok(await IOUtils.exists(folderpath), "folder is not removable");
    734  return { path: testFile, worker };
    735 }
    736 
    737 /**
    738 * Stop the worker returned from openUniqueFileInFolder and close the file.
    739 *
    740 * @param {object} worker The worker returned by openUniqueFileInFolder
    741 */
    742 async function closeTestFile(worker) {
    743  await worker.post("close", []);
    744 }
    745 
    746 /**
    747 * Run a backup and check that it either succeeded with a response or failed
    748 * and returned null.
    749 *
    750 * @param {object}  backupService Instance of BackupService
    751 * @param {string}  profilePath   Full path to profile folder
    752 * @param {boolean} shouldSucceed Whether to expect success or failure
    753 */
    754 async function checkBackup(backupService, profilePath, shouldSucceed) {
    755  if (shouldSucceed) {
    756    await backupService.createBackup({ profilePath }).then(result => {
    757      Assert.ok(true, "createBackup did not throw an exception");
    758      Assert.notEqual(
    759        result,
    760        null,
    761        `createBackup should not have returned null`
    762      );
    763    });
    764    await backupService.deleteLastBackup();
    765    return;
    766  }
    767 
    768  await Assert.rejects(
    769    backupService.createBackup({ profilePath }),
    770    /Failed to remove/,
    771    "createBackup threw correct exception"
    772  );
    773 }
    774 
    775 /**
    776 * Checks that browser.backup.max-num-unremovable-staging-items allows backups
    777 * to succeed if the snapshots folder contains no more than that many
    778 * unremovable items, and that it fails if there are more than that.
    779 *
    780 * @param {number} unremovableItemsLimit Max number of unremovable items that
    781 *                                       backups can succeed with.
    782 */
    783 async function checkBackupWithUnremovableItems(unremovableItemsLimit) {
    784  Services.prefs.setIntPref(
    785    "browser.backup.max-num-unremovable-staging-items",
    786    unremovableItemsLimit
    787  );
    788  registerCleanupFunction(() =>
    789    Services.prefs.clearUserPref(
    790      "browser.backup.max-num-unremovable-staging-items"
    791    )
    792  );
    793 
    794  let sandbox = sinon.createSandbox();
    795 
    796  const TEST_UID = "ThisIsMyTestUID";
    797  const TEST_EMAIL = "foxy@mozilla.org";
    798 
    799  sandbox.stub(UIState, "get").returns({
    800    status: UIState.STATUS_SIGNED_IN,
    801    uid: TEST_UID,
    802    email: TEST_EMAIL,
    803  });
    804  const backupService = new BackupService({});
    805 
    806  let profilePath = await IOUtils.createUniqueDirectory(
    807    PathUtils.tempDir,
    808    "profileDir"
    809  );
    810  let snapshotsFolder = PathUtils.join(
    811    profilePath,
    812    BackupService.PROFILE_FOLDER_NAME,
    813    BackupService.SNAPSHOTS_FOLDER_NAME
    814  );
    815 
    816  let openFileWorkers = [];
    817  try {
    818    for (let i = 0; i < unremovableItemsLimit + 1; i++) {
    819      info(`Performing backup #${i}`);
    820      await checkBackup(backupService, profilePath, true /* shouldSucceed */);
    821 
    822      // Create and open a file so that the snapshots folder cannot be
    823      // emptied.
    824      openFileWorkers.push(await openUniqueFileInFolder(snapshotsFolder));
    825    }
    826 
    827    // We are now over the unremovableItemsLimit.
    828    info(`Performing backup that should fail`);
    829    await checkBackup(backupService, profilePath, false /* shouldSucceed */);
    830  } finally {
    831    await Promise.all(
    832      openFileWorkers.map(async ofw => await closeTestFile(ofw.worker))
    833    );
    834    await Promise.all(
    835      openFileWorkers.map(async ofw => await IOUtils.remove(ofw.path))
    836    );
    837    await IOUtils.remove(profilePath, { recursive: true });
    838    sandbox.restore();
    839  }
    840 }
    841 
    842 /**
    843 * Tests that any non-read-only file deletion errors do not prevent backups
    844 * until the browser.backup.max-num-unremovable-staging-items limit has been
    845 * reached.
    846 */
    847 add_task(
    848  async function test_createBackup_robustToNonReadonlyFileSystemErrorsAllowOneNonReadonly() {
    849    await checkBackupWithUnremovableItems(1);
    850  }
    851 );
    852 
    853 /**
    854 * Tests that browser.backup.max-num-unremovable-staging-items works for value
    855 * 0.
    856 */
    857 add_task(
    858  async function test_createBackup_robustToNonReadonlyFileSystemErrors() {
    859    await checkBackupWithUnremovableItems(0);
    860  }
    861 );
    862 
    863 /**
    864 * Tests that failure to delete the prior backup doesn't prevent the backup
    865 * location from being edited.
    866 */
    867 add_task(
    868  async function test_editBackupLocation_robustToDeleteLastBackupException() {
    869    const backupLocationPref = "browser.backup.location";
    870    const resetLocation = Services.prefs.getStringPref(backupLocationPref);
    871 
    872    const exceptionBackupLocation = await IOUtils.createUniqueDirectory(
    873      PathUtils.tempDir,
    874      "exceptionBackupLocation"
    875    );
    876    Services.prefs.setStringPref(backupLocationPref, exceptionBackupLocation);
    877 
    878    const newBackupLocation = await IOUtils.createUniqueDirectory(
    879      PathUtils.tempDir,
    880      "newBackupLocation"
    881    );
    882 
    883    let pickerDir = await IOUtils.getDirectory(newBackupLocation);
    884    const reg = MockRegistrar.register("@mozilla.org/filepicker;1", {
    885      init() {},
    886      open(cb) {
    887        cb.done(Ci.nsIFilePicker.returnOK);
    888      },
    889      displayDirectory: null,
    890      file: pickerDir,
    891      QueryInterface: ChromeUtils.generateQI(["nsIFilePicker"]),
    892    });
    893 
    894    const backupService = new BackupService({});
    895 
    896    const sandbox = sinon.createSandbox();
    897    sandbox
    898      .stub(backupService, "deleteLastBackup")
    899      .rejects(new Error("Exception while deleting backup"));
    900 
    901    await backupService.editBackupLocation({ browsingContext: null });
    902 
    903    pickerDir.append("Restore Firefox");
    904    Assert.equal(
    905      Services.prefs.getStringPref(backupLocationPref),
    906      pickerDir.path,
    907      "Backup location pref should have updated to the new directory."
    908    );
    909 
    910    Services.prefs.setStringPref(backupLocationPref, resetLocation);
    911    sinon.restore();
    912    MockRegistrar.unregister(reg);
    913    await Promise.all([
    914      IOUtils.remove(exceptionBackupLocation, { recursive: true }),
    915      IOUtils.remove(newBackupLocation, { recursive: true }),
    916    ]);
    917  }
    918 );
    919 
    920 /**
    921 * Tests that the existence of selectable profiles prevent backups (see bug
    922 * 1990980).
    923 *
    924 * @param {boolean} aSetCreatedSelectableProfilesBeforeSchedulingBackups
    925 *    If true (respectively, false), set browser.profiles.created before
    926 *    (respectively, after) attempting to setScheduledBackups.
    927 */
    928 async function testSelectableProfilesPreventBackup(
    929  aSetCreatedSelectableProfilesBeforeSchedulingBackups
    930 ) {
    931  let sandbox = sinon.createSandbox();
    932  Services.fog.testResetFOG();
    933  const TEST_UID = "ThisIsMyTestUID";
    934  const TEST_EMAIL = "foxy@mozilla.org";
    935  sandbox.stub(UIState, "get").returns({
    936    status: UIState.STATUS_SIGNED_IN,
    937    uid: TEST_UID,
    938    email: TEST_EMAIL,
    939  });
    940 
    941  const SELECTABLE_PROFILES_CREATED_PREF = "browser.profiles.created";
    942 
    943  Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, true);
    944  Services.prefs.setBoolPref(BACKUP_RESTORE_ENABLED_PREF_NAME, true);
    945 
    946  // Make sure created profiles pref is not set until we want it to be.
    947  Services.prefs.setBoolPref(SELECTABLE_PROFILES_CREATED_PREF, false);
    948 
    949  const setHasSelectableProfiles = () => {
    950    // "Enable" selectable profiles by pref.
    951    Services.prefs.setBoolPref(SELECTABLE_PROFILES_CREATED_PREF, true);
    952    Assert.ok(
    953      Services.prefs.getBoolPref(SELECTABLE_PROFILES_CREATED_PREF),
    954      "set has selectable profiles | browser.profiles.created = true"
    955    );
    956  };
    957 
    958  if (aSetCreatedSelectableProfilesBeforeSchedulingBackups) {
    959    setHasSelectableProfiles();
    960  }
    961 
    962  let bs = new BackupService({});
    963  bs.initBackupScheduler();
    964  bs.setScheduledBackups(true);
    965 
    966  const SCHEDULED_BACKUP_ENABLED_PREF = "browser.backup.scheduled.enabled";
    967  if (!aSetCreatedSelectableProfilesBeforeSchedulingBackups) {
    968    Assert.ok(
    969      Services.prefs.getBoolPref(SCHEDULED_BACKUP_ENABLED_PREF, true),
    970      "enabled scheduled backups | browser.backup.scheduled.enabled = true"
    971    );
    972    registerCleanupFunction(() => {
    973      // Just in case the test fails.
    974      bs.setScheduledBackups(false);
    975      info("cleared scheduled backups");
    976    });
    977 
    978    setHasSelectableProfiles();
    979  }
    980 
    981  // Backups attempts should be rejected because of selectable profiles.
    982  let fakeProfilePath = await IOUtils.createUniqueDirectory(
    983    PathUtils.tempDir,
    984    "testSelectableProfilesPreventBackup"
    985  );
    986  registerCleanupFunction(async () => {
    987    await maybeRemovePath(fakeProfilePath);
    988  });
    989  let failedBackup = await bs.createBackup({
    990    profilePath: fakeProfilePath,
    991  });
    992  Assert.equal(failedBackup, null, "Backup returned null");
    993 
    994  // Test cleanup
    995  if (!aSetCreatedSelectableProfilesBeforeSchedulingBackups) {
    996    bs.uninitBackupScheduler();
    997  }
    998 
    999  Services.prefs.clearUserPref(SELECTABLE_PROFILES_CREATED_PREF);
   1000  // These tests assume that backups and restores have been enabled.
   1001  Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, true);
   1002  Services.prefs.setBoolPref(BACKUP_RESTORE_ENABLED_PREF_NAME, true);
   1003  sandbox.restore();
   1004 }
   1005 
   1006 add_task(
   1007  async function test_managing_profiles_before_scheduling_prevents_backup() {
   1008    await testSelectableProfilesPreventBackup(
   1009      true /* aSetCreatedSelectableProfilesBeforeSchedulingBackups */
   1010    );
   1011  }
   1012 );
   1013 
   1014 add_task(
   1015  async function test_managing_profiles_after_scheduling_prevents_backup() {
   1016    await testSelectableProfilesPreventBackup(
   1017      false /* aSetCreatedSelectableProfilesBeforeSchedulingBackups */
   1018    );
   1019  }
   1020 );
   1021 
   1022 /**
   1023 * Tests that if there's a post-recovery.json file in the profile directory
   1024 * when checkForPostRecovery() is called, that it is processed, and the
   1025 * postRecovery methods on the associated BackupResources are called with the
   1026 * entry values from the file.
   1027 */
   1028 add_task(async function test_checkForPostRecovery() {
   1029  let sandbox = sinon.createSandbox();
   1030 
   1031  let testProfilePath = await IOUtils.createUniqueDirectory(
   1032    PathUtils.tempDir,
   1033    "checkForPostRecoveryTest"
   1034  );
   1035  let fakePostRecoveryObject = {
   1036    [FakeBackupResource1.key]: "test 1",
   1037    [FakeBackupResource3.key]: "test 3",
   1038  };
   1039  await IOUtils.writeJSON(
   1040    PathUtils.join(testProfilePath, BackupService.POST_RECOVERY_FILE_NAME),
   1041    fakePostRecoveryObject
   1042  );
   1043 
   1044  sandbox.stub(FakeBackupResource1.prototype, "postRecovery").resolves();
   1045  sandbox.stub(FakeBackupResource2.prototype, "postRecovery").resolves();
   1046  sandbox.stub(FakeBackupResource3.prototype, "postRecovery").resolves();
   1047 
   1048  let bs = new BackupService({
   1049    FakeBackupResource1,
   1050    FakeBackupResource2,
   1051    FakeBackupResource3,
   1052  });
   1053 
   1054  await bs.checkForPostRecovery(testProfilePath);
   1055  await bs.postRecoveryComplete;
   1056 
   1057  Assert.ok(
   1058    FakeBackupResource1.prototype.postRecovery.calledOnce,
   1059    "FakeBackupResource1.postRecovery was called once"
   1060  );
   1061  Assert.ok(
   1062    FakeBackupResource2.prototype.postRecovery.notCalled,
   1063    "FakeBackupResource2.postRecovery was not called"
   1064  );
   1065  Assert.ok(
   1066    FakeBackupResource3.prototype.postRecovery.calledOnce,
   1067    "FakeBackupResource3.postRecovery was called once"
   1068  );
   1069  Assert.ok(
   1070    FakeBackupResource1.prototype.postRecovery.calledWith(
   1071      fakePostRecoveryObject[FakeBackupResource1.key]
   1072    ),
   1073    "FakeBackupResource1.postRecovery was called with the expected argument"
   1074  );
   1075  Assert.ok(
   1076    FakeBackupResource3.prototype.postRecovery.calledWith(
   1077      fakePostRecoveryObject[FakeBackupResource3.key]
   1078    ),
   1079    "FakeBackupResource3.postRecovery was called with the expected argument"
   1080  );
   1081 
   1082  await IOUtils.remove(testProfilePath, { recursive: true });
   1083  sandbox.restore();
   1084 });
   1085 
   1086 /**
   1087 * Tests that getBackupFileInfo updates backupFileInfo in the state with a subset
   1088 * of info from the fake SampleArchiveResult returned by sampleArchive().
   1089 */
   1090 add_task(async function test_getBackupFileInfo() {
   1091  let sandbox = sinon.createSandbox();
   1092 
   1093  let fakeSampleArchiveResult = {
   1094    isEncrypted: IS_ENCRYPTED,
   1095    startByteOffset: 26985,
   1096    contentType: "multipart/mixed",
   1097    archiveJSON: {
   1098      version: 1,
   1099      meta: {
   1100        date: DATE,
   1101        deviceName: DEVICE_NAME,
   1102        appName: APP_NAME,
   1103        appVersion: APP_VERSION,
   1104        buildID: BUILD_ID,
   1105        osName: OS_NAME,
   1106        osVersion: OS_VERSION,
   1107        healthTelemetryEnabled: TELEMETRY_ENABLED,
   1108        legacyClientID: LEGACY_CLIENT_ID,
   1109      },
   1110      encConfig: {},
   1111    },
   1112  };
   1113 
   1114  sandbox
   1115    .stub(BackupService.prototype, "sampleArchive")
   1116    .resolves(fakeSampleArchiveResult);
   1117 
   1118  let bs = new BackupService();
   1119 
   1120  await bs.getBackupFileInfo("fake-archive.html");
   1121 
   1122  Assert.ok(
   1123    BackupService.prototype.sampleArchive.calledOnce,
   1124    "sampleArchive was called once"
   1125  );
   1126 
   1127  Assert.deepEqual(
   1128    bs.state.backupFileInfo,
   1129    {
   1130      isEncrypted: IS_ENCRYPTED,
   1131      date: DATE,
   1132      deviceName: DEVICE_NAME,
   1133      appName: APP_NAME,
   1134      appVersion: APP_VERSION,
   1135      buildID: BUILD_ID,
   1136      osName: OS_NAME,
   1137      osVersion: OS_VERSION,
   1138      healthTelemetryEnabled: TELEMETRY_ENABLED,
   1139      legacyClientID: LEGACY_CLIENT_ID,
   1140    },
   1141    "State should match a subset from the archive sample."
   1142  );
   1143 
   1144  sandbox.restore();
   1145 });
   1146 
   1147 /**
   1148 * Tests that deleting the last backup will delete the last known backup file if
   1149 * it exists, and will clear the last backup timestamp and filename state
   1150 * properties and preferences.
   1151 */
   1152 add_task(async function test_deleteLastBackup_file_exists() {
   1153  await testDeleteLastBackupHelper();
   1154 });
   1155 
   1156 /**
   1157 * Tests that deleting the last backup does not reject if the last backup file
   1158 * does not exist, and will still clear the last backup timestamp and filename
   1159 * state properties and preferences.
   1160 */
   1161 add_task(async function test__deleteLastBackup_file_does_not_exist() {
   1162  // Now delete the file ourselves before we call deleteLastBackup,
   1163  // so that it's missing from the disk.
   1164  await testDeleteLastBackupHelper(async lastBackupFilePath => {
   1165    await maybeRemovePath(lastBackupFilePath);
   1166  });
   1167 });
   1168 
   1169 /**
   1170 * Tests that getBackupFileInfo properly handles errors, and clears file info
   1171 * for errors that indicate that the file is invalid.
   1172 */
   1173 add_task(async function test_getBackupFileInfo_error_handling() {
   1174  let sandbox = sinon.createSandbox();
   1175 
   1176  const errorTypes = [
   1177    ERRORS.FILE_SYSTEM_ERROR,
   1178    ERRORS.CORRUPTED_ARCHIVE,
   1179    ERRORS.UNSUPPORTED_BACKUP_VERSION,
   1180    ERRORS.INTERNAL_ERROR,
   1181    ERRORS.UNINITIALIZED,
   1182    ERRORS.INVALID_PASSWORD,
   1183  ];
   1184 
   1185  for (const testError of errorTypes) {
   1186    let bs = new BackupService();
   1187 
   1188    let fakeSampleArchiveResult = {
   1189      isEncrypted: IS_ENCRYPTED,
   1190      startByteOffset: 26985,
   1191      contentType: "multipart/mixed",
   1192      archiveJSON: {
   1193        version: 1,
   1194        meta: {
   1195          date: DATE,
   1196          deviceName: DEVICE_NAME,
   1197          appName: APP_NAME,
   1198          appVersion: APP_VERSION,
   1199          buildID: BUILD_ID,
   1200          osName: OS_NAME,
   1201          osVersion: OS_VERSION,
   1202          healthTelemetryEnabled: TELEMETRY_ENABLED,
   1203          legacyClientID: LEGACY_CLIENT_ID,
   1204        },
   1205        encConfig: {},
   1206      },
   1207    };
   1208 
   1209    sandbox
   1210      .stub(BackupService.prototype, "sampleArchive")
   1211      .resolves(fakeSampleArchiveResult);
   1212    await bs.getBackupFileInfo("test-backup.html");
   1213 
   1214    // Verify initial state was set
   1215    Assert.deepEqual(
   1216      bs.state.backupFileInfo,
   1217      {
   1218        isEncrypted: IS_ENCRYPTED,
   1219        date: DATE,
   1220        deviceName: DEVICE_NAME,
   1221        appName: APP_NAME,
   1222        appVersion: APP_VERSION,
   1223        buildID: BUILD_ID,
   1224        osName: OS_NAME,
   1225        osVersion: OS_VERSION,
   1226        healthTelemetryEnabled: TELEMETRY_ENABLED,
   1227        legacyClientID: LEGACY_CLIENT_ID,
   1228      },
   1229      "Initial state should be set correctly"
   1230    );
   1231    Assert.strictEqual(
   1232      bs.state.backupFileToRestore,
   1233      "test-backup.html",
   1234      "Initial backupFileToRestore should be set correctly"
   1235    );
   1236 
   1237    // Test when sampleArchive throws an error
   1238    sandbox.restore();
   1239    sandbox
   1240      .stub(BackupService.prototype, "sampleArchive")
   1241      .rejects(new Error("Test error", { cause: testError }));
   1242    const setRecoveryErrorStub = sandbox.stub(bs, "setRecoveryError");
   1243 
   1244    try {
   1245      await bs.getBackupFileInfo("test-backup.html");
   1246    } catch (error) {
   1247      Assert.ok(
   1248        false,
   1249        `Expected getBackupFileInfo to throw for error ${testError}`
   1250      );
   1251    }
   1252 
   1253    Assert.ok(
   1254      setRecoveryErrorStub.calledOnceWith(testError),
   1255      `setRecoveryError should be called with ${testError}`
   1256    );
   1257 
   1258    Assert.strictEqual(
   1259      bs.state.backupFileInfo,
   1260      null,
   1261      `backupFileInfo should be cleared for error ${testError}`
   1262    );
   1263    Assert.strictEqual(
   1264      bs.state.backupFileToRestore,
   1265      null,
   1266      `backupFileToRestore should be cleared for error ${testError}`
   1267    );
   1268 
   1269    sandbox.restore();
   1270  }
   1271 });
   1272 
   1273 /**
   1274 * Tests changing the status prefs to ensure that backup is cleaned up if being disabled.
   1275 */
   1276 add_task(async function test_changing_prefs_cleanup() {
   1277  let sandbox = sinon.createSandbox();
   1278  Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, true);
   1279  let bs = new BackupService();
   1280  bs.initStatusObservers();
   1281  let cleanupStub = sandbox.stub(bs, "cleanupBackupFiles");
   1282  let statusUpdatePromise = TestUtils.topicObserved(
   1283    "backup-service-status-updated"
   1284  );
   1285  Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, false);
   1286  await statusUpdatePromise;
   1287 
   1288  Assert.equal(
   1289    cleanupStub.callCount,
   1290    1,
   1291    "Cleanup backup files was called on pref change"
   1292  );
   1293 
   1294  Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, true);
   1295 
   1296  Assert.equal(
   1297    cleanupStub.callCount,
   1298    1,
   1299    "Cleanup backup files should not have been called when enabling backups"
   1300  );
   1301 
   1302  Services.prefs.clearUserPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME);
   1303 });
   1304 
   1305 add_task(function test_checkOsSupportsBackup_win10() {
   1306  const osParams = {
   1307    name: "Windows_NT",
   1308    version: "10.0",
   1309    build: "20000",
   1310  };
   1311  const result = BackupService.checkOsSupportsBackup(osParams);
   1312  Assert.ok(result);
   1313 });
   1314 
   1315 add_task(function test_checkOsSupportsBackup_win11() {
   1316  const osParams = {
   1317    name: "Windows_NT",
   1318    version: "10.0",
   1319    build: "22000",
   1320  };
   1321  const result = BackupService.checkOsSupportsBackup(osParams);
   1322  Assert.ok(!result);
   1323 });
   1324 
   1325 add_task(function test_checkOsSupportsBackup_linux() {
   1326  const osParams = {
   1327    name: "Linux",
   1328    version: "10.0",
   1329    build: "22000",
   1330  };
   1331  const result = BackupService.checkOsSupportsBackup(osParams);
   1332  Assert.ok(!result);
   1333 });