tor-browser

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

test_BackupService_telemetry.js (8475B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * https://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 ChromeUtils.defineLazyGetter(this, "nsLocalFile", () =>
      7  Components.Constructor("@mozilla.org/file/local;1", "nsIFile", "initWithPath")
      8 );
      9 
     10 const BACKUP_DIR_PREF_NAME = "browser.backup.location";
     11 
     12 const TEST_PASSWORD = "correcthorsebatterystaple";
     13 
     14 const kKnownMappings = Object.freeze({
     15  OneDrPD: "onedrive",
     16  Docs: "documents",
     17 });
     18 
     19 const gDirectoryServiceProvider = {
     20  getFile(prop, persistent) {
     21    persistent.value = false;
     22 
     23    // We only expect a narrow range of calls.
     24    let folder = gBase.clone();
     25    if (prop === "ProfD") {
     26      return folder;
     27    }
     28 
     29    if (prop in kKnownMappings) {
     30      folder.append("dirsvc");
     31      folder.append(prop + "-dir");
     32      return folder;
     33    }
     34 
     35    console.error(`Access to unexpected directory '${prop}'`);
     36    return Cr.NS_ERROR_FAILURE;
     37  },
     38  QueryInterface: ChromeUtils.generateQI([Ci.nsIDirectoryServiceProvider]),
     39 };
     40 
     41 let gBase;
     42 add_setup(function setup() {
     43  setupProfile();
     44  gBase = do_get_profile();
     45 
     46  Services.dirsvc
     47    .QueryInterface(Ci.nsIDirectoryService)
     48    .registerProvider(gDirectoryServiceProvider);
     49 });
     50 
     51 /**
     52 * Gets a telemetry event and checks that it looks the same between Glean and
     53 * legacy telemetry, i.e. that the extra data is equal.
     54 *
     55 * @param {string} name
     56 *   The Glean programming name of the event, e.g. turnOn instead of turn_on.
     57 * @returns {object}
     58 *   The extra data associated with the event.
     59 */
     60 function assertSingleTelemetryEvent(name) {
     61  let value = Glean.browserBackup[name].testGetValue();
     62  Assert.equal(value.length, 1, `${name} Glean event was recorded once.`);
     63 
     64  let snakeName = name.replace(/([A-Z])/g, "_$1").toLowerCase();
     65  let legacy = TelemetryTestUtils.getEvents(
     66    { category: "browser.backup", method: snakeName, object: "BackupService" },
     67    { process: "parent" }
     68  );
     69  Assert.equal(legacy.length, 1, `${name} legacy event was recorded once.`);
     70 
     71  Assert.deepEqual(
     72    legacy[0].extra,
     73    value[0].extra,
     74    "Legacy telemetry measured the same data as Glean."
     75  );
     76  return value[0].extra;
     77 }
     78 
     79 /**
     80 * Checks that the recorded event's 'encrypted' and 'location' extra keys
     81 * match `destPath` and `encrypted`. Reset telemetry before if needed!
     82 *
     83 * @param {string} name
     84 *   The name of the Glean event that should have been recorded.
     85 * @param {string} destPath
     86 *   The path that the backup was stored to.
     87 * @param {boolean} encrypted
     88 *   Whether the backup was encrypted or not.
     89 */
     90 function assertEventMatches(name, destPath, encrypted) {
     91  let extra = assertSingleTelemetryEvent(name);
     92  Assert.equal(
     93    extra.encrypted,
     94    String(encrypted),
     95    `Glean event indicates the backup is ${encrypted ? "" : "NOT "}encrypted.`
     96  );
     97 
     98  // This is returned from the mock of classifyLocationForTelemetry, and
     99  // checks that the correct path was passed in.
    100  Assert.equal(
    101    extra.location,
    102    `[classifying: ${relativeToProfile(destPath)}]`,
    103    "Glean event has right location"
    104  );
    105 
    106  return extra;
    107 }
    108 
    109 /**
    110 * Determines the path to 'source' from the profile directory to reduce the
    111 * length and avoid truncation within legacy telemetry.
    112 *
    113 * @param {string} path
    114 *   The file that should be pointed to.
    115 * @returns {string}
    116 *   The relative path from 'base' to 'source'.
    117 */
    118 function relativeToProfile(path) {
    119  let file = nsLocalFile(path);
    120  return file.getRelativePath(gBase);
    121 }
    122 
    123 add_task(function test_relativeToProfile() {
    124  // This aims to check that the direction is right.
    125  const file = gBase.clone();
    126  file.append("abc");
    127  Assert.equal(
    128    relativeToProfile(file.path),
    129    "abc",
    130    "relativeToProfile computes the right path."
    131  );
    132 });
    133 
    134 add_task(async function test_created_encrypted_noreason() {
    135  await template("testCreatedEncryptedNoReason", true, undefined);
    136 });
    137 
    138 add_task(async function test_created_nonencrypted_noreason() {
    139  await template("testCreatedNonencryptedNoReason", false, undefined);
    140 });
    141 
    142 add_task(async function test_created_encrypted_with_reason() {
    143  await template("testCreatedEncryptedWithReason", true, "I said so");
    144 });
    145 
    146 async function template(name, encrypted, reason) {
    147  let bs = new BackupService();
    148  let profilePath = await IOUtils.createUniqueDirectory(
    149    PathUtils.tempDir,
    150    name
    151  );
    152 
    153  const backupDir = PathUtils.join(PathUtils.tempDir, name + "_dest");
    154  Services.prefs.setStringPref(BACKUP_DIR_PREF_NAME, backupDir);
    155 
    156  if (encrypted) {
    157    await bs.enableEncryption(TEST_PASSWORD, profilePath);
    158  }
    159 
    160  sinon.stub(bs, "classifyLocationForTelemetry").callsFake(file => {
    161    return `[classifying: ${relativeToProfile(file)}]`;
    162  });
    163 
    164  // To ensure that the backup_start event happens before the actual backup,
    165  // take the lock for ourselves. Then we can unblock the backup once we've
    166  // checked the telemetry is finished.
    167  let resolver = Promise.withResolvers();
    168  locks.request(BackupService.WRITE_BACKUP_LOCK_NAME, () => {
    169    Services.fog.testResetFOG();
    170    Services.telemetry.clearEvents();
    171 
    172    let promise = bs.createBackup({ profilePath, reason });
    173 
    174    let startedEvents = Glean.browserBackup.backupStart.testGetValue();
    175    Assert.equal(
    176      startedEvents.length,
    177      1,
    178      "Found the backup_start Glean event."
    179    );
    180    Assert.equal(
    181      startedEvents[0].extra.reason,
    182      reason ?? "unknown",
    183      "Found the reason for starting the backup in the Glean event."
    184    );
    185 
    186    // Don't await on it, since createBackup needs the lock!
    187    resolver.resolve(promise);
    188  });
    189 
    190  await resolver.promise;
    191 
    192  let value = assertEventMatches("created", backupDir, encrypted);
    193  // Not sure how big it is, and we're not testing the fuzzByteSize
    194  // function, so just check that it's plausible.
    195  Assert.greater(Number(value.size), 0, "Telemetry event has nonzero size");
    196 }
    197 
    198 add_task(async function test_toggleOn() {
    199  let bs = new BackupService();
    200 
    201  let backupDir = PathUtils.join(PathUtils.tempDir, "toggleOn_dest");
    202  Services.prefs.setStringPref(BACKUP_DIR_PREF_NAME, backupDir);
    203 
    204  let profilePath = await IOUtils.createUniqueDirectory(
    205    PathUtils.tempDir,
    206    "toggleOn"
    207  );
    208 
    209  if (bs.state.scheduledBackupsEnabled) {
    210    // The test assumes that this is false. Do this before resetting telemetry
    211    // so it doesn't affect the results.
    212    bs.onUpdateScheduledBackups(false);
    213  }
    214 
    215  sinon.stub(bs, "classifyLocationForTelemetry").callsFake(file => {
    216    return `[classifying: ${relativeToProfile(file)}]`;
    217  });
    218 
    219  Services.fog.testResetFOG();
    220  Services.telemetry.clearEvents();
    221  bs.onUpdateScheduledBackups(true);
    222  assertEventMatches("toggleOn", backupDir, false);
    223 
    224  Services.fog.testResetFOG();
    225  Services.telemetry.clearEvents();
    226  bs.onUpdateScheduledBackups(false);
    227  assertSingleTelemetryEvent("toggleOff");
    228 
    229  await bs.enableEncryption(TEST_PASSWORD, profilePath);
    230  Services.fog.testResetFOG();
    231  Services.telemetry.clearEvents();
    232  bs.onUpdateScheduledBackups(true);
    233  assertEventMatches("toggleOn", backupDir, true);
    234 
    235  Services.fog.testResetFOG();
    236  Services.telemetry.clearEvents();
    237  bs.onUpdateScheduledBackups(false);
    238  assertSingleTelemetryEvent("toggleOff");
    239 });
    240 
    241 add_task(async function test_classifyLocationForTelemetry() {
    242  let bs = new BackupService();
    243  for (const prop of Object.keys(kKnownMappings)) {
    244    let file = Services.dirsvc.get(prop, Ci.nsIFile);
    245    Assert.equal(
    246      bs.classifyLocationForTelemetry(file.path),
    247      "other",
    248      `'${file.path}' was correctly classified.`
    249    );
    250 
    251    file.append("child");
    252    Assert.equal(
    253      bs.classifyLocationForTelemetry(file.path),
    254      kKnownMappings[prop],
    255      `'${file.path}' was correctly classified.`
    256    );
    257 
    258    file = file.parent.parent;
    259    Assert.equal(
    260      bs.classifyLocationForTelemetry(file.path),
    261      "other",
    262      `'${file.path}' was correctly classified.`
    263    );
    264  }
    265 
    266  Assert.equal(
    267    bs.classifyLocationForTelemetry(gBase.path),
    268    "other",
    269    "Unrelated path is not classified anywhere."
    270  );
    271 
    272  Assert.equal(
    273    bs.classifyLocationForTelemetry("path"),
    274    "Error: NS_ERROR_FILE_UNRECOGNIZED_PATH",
    275    "Invalid path returns an error name."
    276  );
    277 });
    278 
    279 add_task(async function test_idleDispatchPassesOptionsThrough() {
    280  let bs = new BackupService();
    281  let stub = sinon.stub(bs, "createBackupOnIdleDispatch").resolves();
    282 
    283  let options = {};
    284  bs.createBackupOnIdleDispatch(options);
    285  Assert.equal(
    286    stub.firstCall.args[0],
    287    options,
    288    "Options were passed as-is into createBackup."
    289  );
    290 });