tor-browser

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

test_ProfileCounter.js (8570B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/
      3 */
      4 "use strict";
      5 
      6 const { BrowserUsageTelemetry } = ChromeUtils.importESModule(
      7  "resource:///modules/BrowserUsageTelemetry.sys.mjs"
      8 );
      9 const { TelemetryTestUtils } = ChromeUtils.importESModule(
     10  "resource://testing-common/TelemetryTestUtils.sys.mjs"
     11 );
     12 
     13 const PROFILE_COUNT_SCALAR = "browser.engagement.profile_count";
     14 // Largest possible uint32_t value represents an error.
     15 const SCALAR_ERROR_VALUE = 0;
     16 
     17 const FILE_OPEN_OPERATION = "open";
     18 const ERROR_FILE_NOT_FOUND = "NotFoundError";
     19 const ERROR_ACCESS_DENIED = "NotAllowedError";
     20 
     21 // We will redirect I/O to/from the profile counter file to read/write this
     22 // variable instead. That makes it easier for us to:
     23 //   - avoid interference from any pre-existing file
     24 //   - read and change the values in the file.
     25 //   - clean up changes made to the file
     26 // We will translate a null value stored here to a File Not Found error.
     27 var gFakeProfileCounterFile = null;
     28 // We will use this to check that the profile counter code doesn't try to write
     29 // to multiple files (since this test will malfunction in that case due to
     30 // gFakeProfileCounterFile only being setup to accommodate a single file).
     31 var gProfileCounterFilePath = null;
     32 
     33 // Storing a value here lets us test the behavior when we encounter an error
     34 // reading or writing to the file. A null value means that no error will
     35 // be simulated (other than possibly a NotFoundError).
     36 var gNextReadExceptionReason = null;
     37 var gNextWriteExceptionReason = null;
     38 
     39 // Nothing will actually be stored in this directory, so it's not important that
     40 // it be valid, but the leafname should be unique to this test in order to be
     41 // sure of preventing name conflicts with the pref:
     42 // `browser.engagement.profileCounted.${hash}`
     43 function getDummyUpdateDirectory() {
     44  const testName = "test_ProfileCounter";
     45  let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
     46  dir.initWithPath(`C:\\foo\\bar\\${testName}`);
     47  return dir;
     48 }
     49 
     50 // We aren't going to bother generating anything looking like a real client ID
     51 // for this. The only real requirements for client ids is that they not repeat
     52 // and that they be strings. So we'll just return an integer as a string and
     53 // increment it when we want a new client id.
     54 var gDummyTelemetryClientId = 0;
     55 function getDummyTelemetryClientId() {
     56  return gDummyTelemetryClientId.toString();
     57 }
     58 function setNewDummyTelemetryClientId() {
     59  ++gDummyTelemetryClientId;
     60 }
     61 
     62 // Returns null if the (fake) profile count file hasn't been created yet.
     63 function getProfileCount() {
     64  // Strict equality to ensure distinguish properly between a non-existent
     65  // file and an empty one.
     66  if (gFakeProfileCounterFile === null) {
     67    return null;
     68  }
     69  let saveData = JSON.parse(gFakeProfileCounterFile);
     70  return saveData.profileTelemetryIds.length;
     71 }
     72 
     73 // Resets the state to the original state, before the profile count file has
     74 // even been written.
     75 // If resetFile is specified as false, this will reset everything except for the
     76 // file itself. This allows us to sort of pretend that another installation
     77 // wrote the file.
     78 function reset(resetFile = true) {
     79  if (resetFile) {
     80    gFakeProfileCounterFile = null;
     81  }
     82  gNextReadExceptionReason = null;
     83  gNextWriteExceptionReason = null;
     84  setNewDummyTelemetryClientId();
     85 }
     86 
     87 function setup() {
     88  reset();
     89  // FOG needs a profile directory to put its data in.
     90  do_get_profile();
     91  // Initialize FOG so we can test the FOG version of profile count
     92  Services.fog.initializeFOG();
     93  Services.fog.testResetFOG();
     94 
     95  BrowserUsageTelemetry.Policy.readProfileCountFile = async path => {
     96    if (!gProfileCounterFilePath) {
     97      gProfileCounterFilePath = path;
     98    } else {
     99      // We've only got one mock-file variable. Make sure we are always
    100      // accessing the same file or this will cause problems.
    101      Assert.equal(
    102        gProfileCounterFilePath,
    103        path,
    104        "Only one file should be accessed"
    105      );
    106    }
    107    // Strict equality to ensure distinguish properly between null and 0.
    108    if (gNextReadExceptionReason !== null) {
    109      let ex = new DOMException(FILE_OPEN_OPERATION, gNextReadExceptionReason);
    110      gNextReadExceptionReason = null;
    111      throw ex;
    112    }
    113    // Strict equality to ensure distinguish properly between a non-existent
    114    // file and an empty one.
    115    if (gFakeProfileCounterFile === null) {
    116      throw new DOMException(FILE_OPEN_OPERATION, ERROR_FILE_NOT_FOUND);
    117    }
    118    return gFakeProfileCounterFile;
    119  };
    120  BrowserUsageTelemetry.Policy.writeProfileCountFile = async (path, data) => {
    121    if (!gProfileCounterFilePath) {
    122      gProfileCounterFilePath = path;
    123    } else {
    124      // We've only got one mock-file variable. Make sure we are always
    125      // accessing the same file or this will cause problems.
    126      Assert.equal(
    127        gProfileCounterFilePath,
    128        path,
    129        "Only one file should be accessed"
    130      );
    131    }
    132    // Strict equality to ensure distinguish properly between null and 0.
    133    if (gNextWriteExceptionReason !== null) {
    134      let ex = new DOMException(FILE_OPEN_OPERATION, gNextWriteExceptionReason);
    135      gNextWriteExceptionReason = null;
    136      throw ex;
    137    }
    138    gFakeProfileCounterFile = data;
    139  };
    140  BrowserUsageTelemetry.Policy.getUpdateDirectory = getDummyUpdateDirectory;
    141  BrowserUsageTelemetry.Policy.getTelemetryClientId = getDummyTelemetryClientId;
    142 }
    143 
    144 // Checks that the number of profiles reported is the number expected. Because
    145 // of bucketing, the raw count may be different than the reported count.
    146 function checkSuccess(profilesReported, rawCount = profilesReported) {
    147  Assert.equal(rawCount, getProfileCount());
    148  const scalars = TelemetryTestUtils.getProcessScalars("parent");
    149  TelemetryTestUtils.assertScalar(
    150    scalars,
    151    PROFILE_COUNT_SCALAR,
    152    profilesReported,
    153    "The value reported to telemetry should be the expected profile count"
    154  );
    155  Assert.equal(
    156    profilesReported,
    157    Glean.browserEngagement.profileCount.testGetValue()
    158  );
    159 }
    160 
    161 function checkError() {
    162  const scalars = TelemetryTestUtils.getProcessScalars("parent");
    163  TelemetryTestUtils.assertScalar(
    164    scalars,
    165    PROFILE_COUNT_SCALAR,
    166    SCALAR_ERROR_VALUE,
    167    "The value reported to telemetry should be the error value"
    168  );
    169 }
    170 
    171 add_task(async function testProfileCounter() {
    172  setup();
    173 
    174  info("Testing basic functionality, single install");
    175  await BrowserUsageTelemetry.reportProfileCount();
    176  checkSuccess(1);
    177  await BrowserUsageTelemetry.reportProfileCount();
    178  checkSuccess(1);
    179 
    180  // Fake another installation by resetting everything except for the profile
    181  // count file.
    182  reset(false);
    183 
    184  info("Testing basic functionality, faking a second install");
    185  await BrowserUsageTelemetry.reportProfileCount();
    186  checkSuccess(2);
    187 
    188  // Check if we properly handle the case where we cannot read from the file
    189  // and we have already set its contents. This should report an error.
    190  info("Testing read error after successful write");
    191  gNextReadExceptionReason = ERROR_ACCESS_DENIED;
    192  await BrowserUsageTelemetry.reportProfileCount();
    193  checkError();
    194 
    195  reset();
    196 
    197  // A read error should cause an error to be reported, but should also write
    198  // to the file in an attempt to fix it. So the next (successful) read should
    199  // result in the correct telemetry.
    200  info("Testing read error self-correction");
    201  gNextReadExceptionReason = ERROR_ACCESS_DENIED;
    202  await BrowserUsageTelemetry.reportProfileCount();
    203  checkError();
    204 
    205  await BrowserUsageTelemetry.reportProfileCount();
    206  checkSuccess(1);
    207 
    208  reset();
    209 
    210  // If the file is malformed. We should report an error and fix it, then report
    211  // the correct profile count next time.
    212  info("Testing with malformed profile count file");
    213  gFakeProfileCounterFile = "<malformed file data>";
    214  await BrowserUsageTelemetry.reportProfileCount();
    215  checkError();
    216 
    217  await BrowserUsageTelemetry.reportProfileCount();
    218  checkSuccess(1);
    219 
    220  reset();
    221 
    222  // If we haven't yet written to the file, a write error should cause an error
    223  // to be reported.
    224  info("Testing write error before the first write");
    225  gNextWriteExceptionReason = ERROR_ACCESS_DENIED;
    226  await BrowserUsageTelemetry.reportProfileCount();
    227  checkError();
    228 
    229  reset();
    230 
    231  info("Testing bucketing");
    232  // Fake 15 installations to drive the raw profile count up to 15.
    233  for (let i = 0; i < 15; i++) {
    234    reset(false);
    235    await BrowserUsageTelemetry.reportProfileCount();
    236  }
    237  // With bucketing, values from 10-99 should all be reported as 10.
    238  checkSuccess(10, 15);
    239 });