tor-browser

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

test_NewTabAttributionService.js (16840B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 https://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 ChromeUtils.defineESModuleGetters(this, {
      7  NewTabAttributionServiceClass:
      8    "resource://newtab/lib/NewTabAttributionService.sys.mjs",
      9  ObliviousHTTP: "resource://gre/modules/ObliviousHTTP.sys.mjs",
     10  AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
     11  sinon: "resource://testing-common/Sinon.sys.mjs",
     12 });
     13 
     14 const { HttpServer } = ChromeUtils.importESModule(
     15  "resource://testing-common/httpd.sys.mjs"
     16 );
     17 
     18 const BinaryInputStream = Components.Constructor(
     19  "@mozilla.org/binaryinputstream;1",
     20  "nsIBinaryInputStream",
     21  "setInputStream"
     22 );
     23 
     24 const PREF_LEADER = "toolkit.telemetry.dap.leader.url";
     25 const PREF_HELPER = "toolkit.telemetry.dap.helper.url";
     26 const TASK_ID = "DSZGMFh26hBYXNaKvhL_N4AHA3P5lDn19on1vFPBxJM";
     27 const MAX_CONVERSIONS = 2;
     28 const DAY_IN_MILLI = 1000 * 60 * 60 * 24;
     29 const LOOKBACK_DAYS = 1;
     30 const MAX_LOOKBACK_DAYS = 30;
     31 const HISTOGRAM_SIZE = 5;
     32 
     33 class MockDateProvider {
     34  constructor() {
     35    this._now = Date.now();
     36  }
     37 
     38  now() {
     39    return this._now;
     40  }
     41 
     42  add(interval_ms) {
     43    this._now += interval_ms;
     44  }
     45 }
     46 
     47 class MockDAPSender {
     48  constructor() {
     49    this.receivedMeasurements = [];
     50  }
     51 
     52  async sendDAPMeasurement(task, measurement, options) {
     53    this.receivedMeasurements.push({
     54      task,
     55      measurement,
     56      options,
     57    });
     58  }
     59 }
     60 
     61 class MockServer {
     62  constructor() {
     63    this.receivedReports = [];
     64 
     65    const server = new HttpServer();
     66 
     67    server.registerPrefixHandler(
     68      "/leader_endpoint/tasks/",
     69      this.uploadHandler.bind(this)
     70    );
     71 
     72    this._server = server;
     73  }
     74 
     75  start() {
     76    this._server.start(-1);
     77 
     78    this.orig_leader = Services.prefs.getStringPref(PREF_LEADER);
     79    this.orig_helper = Services.prefs.getStringPref(PREF_HELPER);
     80 
     81    const i = this._server.identity;
     82    const serverAddr = `${i.primaryScheme}://${i.primaryHost}:${i.primaryPort}`;
     83    Services.prefs.setStringPref(PREF_LEADER, `${serverAddr}/leader_endpoint`);
     84    Services.prefs.setStringPref(PREF_HELPER, `${serverAddr}/helper_endpoint`);
     85  }
     86 
     87  async stop() {
     88    Services.prefs.setStringPref(PREF_LEADER, this.orig_leader);
     89    Services.prefs.setStringPref(PREF_HELPER, this.orig_helper);
     90 
     91    await this._server.stop();
     92  }
     93 
     94  uploadHandler(request, response) {
     95    let body = new BinaryInputStream(request.bodyInputStream);
     96 
     97    this.receivedReports.push({
     98      contentType: request.getHeader("Content-Type"),
     99      size: body.available(),
    100    });
    101 
    102    response.setStatusLine(request.httpVersion, 200);
    103  }
    104 }
    105 
    106 let globalSandbox;
    107 
    108 add_setup(async function () {
    109  do_get_profile();
    110  Services.prefs.setStringPref(
    111    "browser.newtabpage.activity-stream.unifiedAds.endpoint",
    112    "https://test.example.com"
    113  );
    114  Services.prefs.setStringPref(
    115    "browser.newtabpage.activity-stream.discoverystream.ohttp.configURL",
    116    "https://test.example.com/config"
    117  );
    118  Services.prefs.setStringPref(
    119    "browser.newtabpage.activity-stream.discoverystream.ohttp.relayURL",
    120    "https://test.example.com/relay"
    121  );
    122 
    123  globalSandbox = sinon.createSandbox();
    124  globalSandbox.stub(ObliviousHTTP, "getOHTTPConfig").resolves({});
    125  globalSandbox.stub(ObliviousHTTP, "ohttpRequest").resolves({
    126    status: 200,
    127    json: () => {
    128      return Promise.resolve({
    129        task_id: TASK_ID,
    130        vdaf: "histogram",
    131        bits: 1,
    132        length: HISTOGRAM_SIZE,
    133        time_precision: 60,
    134        default_measurement: 0,
    135      });
    136    },
    137  });
    138 
    139  const mockStore = {
    140    getState: () => ({
    141      Prefs: {
    142        values: {
    143          trainhopConfig: {
    144            attribution: {},
    145          },
    146        },
    147      },
    148    }),
    149  };
    150 
    151  globalSandbox.stub(AboutNewTab, "activityStream").value({
    152    store: mockStore,
    153  });
    154 });
    155 
    156 registerCleanupFunction(() => {
    157  Services.prefs.clearUserPref(
    158    "browser.newtabpage.activity-stream.unifiedAds.endpoint"
    159  );
    160  Services.prefs.clearUserPref(
    161    "browser.newtabpage.activity-stream.discoverystream.ohttp.configURL"
    162  );
    163  Services.prefs.clearUserPref(
    164    "browser.newtabpage.activity-stream.discoverystream.ohttp.relayURL"
    165  );
    166 
    167  globalSandbox.restore();
    168 });
    169 
    170 add_task(async function testSuccessfulConversion() {
    171  const mockSender = new MockDAPSender();
    172  const privateAttribution = new NewTabAttributionServiceClass({
    173    dapSender: mockSender,
    174  });
    175 
    176  const partnerIdentifier = "partner_identifier";
    177  const index = 1;
    178 
    179  await privateAttribution.onAttributionEvent("view", {
    180    partner_id: partnerIdentifier,
    181    index,
    182  });
    183 
    184  await privateAttribution.onAttributionEvent("click", {
    185    partner_id: partnerIdentifier,
    186    index,
    187  });
    188 
    189  await privateAttribution.onAttributionConversion(
    190    partnerIdentifier,
    191    LOOKBACK_DAYS,
    192    "view"
    193  );
    194 
    195  const receivedMeasurement = mockSender.receivedMeasurements.pop();
    196  Assert.deepEqual(receivedMeasurement.task, {
    197    task_id: TASK_ID,
    198    id: TASK_ID,
    199    vdaf: "histogram",
    200    bits: 1,
    201    length: HISTOGRAM_SIZE,
    202    time_precision: 60,
    203    default_measurement: 0,
    204  });
    205  Assert.equal(receivedMeasurement.measurement, index);
    206  Assert.ok(receivedMeasurement.options.ohttp_hpke);
    207  Assert.equal(receivedMeasurement.options.ohttp_hpke.length, 41);
    208  Assert.equal(
    209    receivedMeasurement.options.ohttp_relay,
    210    Services.prefs.getStringPref("dap.ohttp.relayURL")
    211  );
    212  Assert.equal(mockSender.receivedMeasurements.length, 0);
    213 });
    214 
    215 add_task(async function testZeroIndex() {
    216  const mockSender = new MockDAPSender();
    217  const privateAttribution = new NewTabAttributionServiceClass({
    218    dapSender: mockSender,
    219  });
    220 
    221  const partnerIdentifier = "partner_identifier_zero";
    222  const index = 0;
    223 
    224  await privateAttribution.onAttributionEvent("view", {
    225    partner_id: partnerIdentifier,
    226    index,
    227  });
    228 
    229  await privateAttribution.onAttributionConversion(
    230    partnerIdentifier,
    231    LOOKBACK_DAYS,
    232    "view"
    233  );
    234 
    235  const receivedMeasurement = mockSender.receivedMeasurements.pop();
    236  Assert.equal(receivedMeasurement.measurement, index);
    237  Assert.equal(mockSender.receivedMeasurements.length, 0);
    238 });
    239 
    240 add_task(async function testConversionWithoutImpression() {
    241  const mockSender = new MockDAPSender();
    242  const privateAttribution = new NewTabAttributionServiceClass({
    243    dapSender: mockSender,
    244  });
    245 
    246  const partnerIdentifier = "partner_identifier_no_impression";
    247 
    248  await privateAttribution.onAttributionConversion(
    249    partnerIdentifier,
    250    LOOKBACK_DAYS,
    251    "view"
    252  );
    253 
    254  const receivedMeasurement = mockSender.receivedMeasurements.pop();
    255  Assert.deepEqual(receivedMeasurement.task, {
    256    task_id: TASK_ID,
    257    id: TASK_ID,
    258    vdaf: "histogram",
    259    bits: 1,
    260    length: HISTOGRAM_SIZE,
    261    time_precision: 60,
    262    default_measurement: 0,
    263  });
    264  Assert.equal(receivedMeasurement.measurement, 0);
    265  Assert.equal(mockSender.receivedMeasurements.length, 0);
    266 });
    267 
    268 add_task(async function testConversionWithInvalidLookbackDays() {
    269  const mockSender = new MockDAPSender();
    270  const privateAttribution = new NewTabAttributionServiceClass({
    271    dapSender: mockSender,
    272  });
    273 
    274  const partnerIdentifier = "partner_identifier";
    275  const index = 1;
    276 
    277  await privateAttribution.onAttributionEvent("view", {
    278    partner_id: partnerIdentifier,
    279    index,
    280  });
    281 
    282  await privateAttribution.onAttributionConversion(
    283    partnerIdentifier,
    284    MAX_LOOKBACK_DAYS + 1,
    285    "view"
    286  );
    287 
    288  Assert.equal(mockSender.receivedMeasurements.length, 0);
    289 });
    290 
    291 add_task(async function testSelectionByLastView() {
    292  const mockSender = new MockDAPSender();
    293  const mockDateProvider = new MockDateProvider();
    294  const privateAttribution = new NewTabAttributionServiceClass({
    295    dapSender: mockSender,
    296    dateProvider: mockDateProvider,
    297  });
    298 
    299  const partnerIdentifier = "partner_identifier_last_view";
    300  const selectedViewIndex = 1;
    301  const ignoredViewIndex = 2;
    302  const clickIndex = 3;
    303 
    304  // View event that will be ignored, as a more recent view will exist
    305  await privateAttribution.onAttributionEvent("view", {
    306    partner_id: partnerIdentifier,
    307    index: ignoredViewIndex,
    308  });
    309 
    310  // step forward time
    311  mockDateProvider.add(10);
    312 
    313  // View event that will be selected, as no more recent view exists
    314  await privateAttribution.onAttributionEvent("view", {
    315    partner_id: partnerIdentifier,
    316    index: selectedViewIndex,
    317  });
    318 
    319  // step forward time
    320  mockDateProvider.add(10);
    321 
    322  // Click event that will be ignored because the match type is "view"
    323  await privateAttribution.onAttributionEvent("click", {
    324    partner_id: partnerIdentifier,
    325    index: clickIndex,
    326  });
    327 
    328  // Conversion filtering for "view" finds the view event
    329  await privateAttribution.onAttributionConversion(
    330    partnerIdentifier,
    331    LOOKBACK_DAYS,
    332    "view"
    333  );
    334 
    335  let receivedMeasurement = mockSender.receivedMeasurements.pop();
    336  Assert.deepEqual(receivedMeasurement.measurement, selectedViewIndex);
    337  Assert.equal(mockSender.receivedMeasurements.length, 0);
    338 });
    339 
    340 add_task(async function testSelectionByLastClick() {
    341  const mockSender = new MockDAPSender();
    342  const mockDateProvider = new MockDateProvider();
    343  const privateAttribution = new NewTabAttributionServiceClass({
    344    dapSender: mockSender,
    345    dateProvider: mockDateProvider,
    346  });
    347 
    348  const partnerIdentifier = "partner_identifier_last_click";
    349  const viewIndex = 1;
    350  const ignoredClickIndex = 2;
    351  const selectedClickIndex = 3;
    352 
    353  // Click event that will be ignored, as a more recent click will exist
    354  await privateAttribution.onAttributionEvent("click", {
    355    partner_id: partnerIdentifier,
    356    index: ignoredClickIndex,
    357  });
    358 
    359  // step forward time
    360  mockDateProvider.add(10);
    361 
    362  // Click event that will be selected, as no more recent click exists
    363  await privateAttribution.onAttributionEvent("click", {
    364    partner_id: partnerIdentifier,
    365    index: selectedClickIndex,
    366  });
    367 
    368  // step forward time
    369  mockDateProvider.add(10);
    370 
    371  // View event that will be ignored because the match type is "click"
    372  await privateAttribution.onAttributionEvent("view", {
    373    partner_id: partnerIdentifier,
    374    index: viewIndex,
    375  });
    376 
    377  // Conversion filtering for "click" finds the click event
    378  await privateAttribution.onAttributionConversion(
    379    partnerIdentifier,
    380    LOOKBACK_DAYS,
    381    "click"
    382  );
    383 
    384  let receivedMeasurement = mockSender.receivedMeasurements.pop();
    385  Assert.deepEqual(receivedMeasurement.measurement, selectedClickIndex);
    386  Assert.equal(mockSender.receivedMeasurements.length, 0);
    387 });
    388 
    389 add_task(async function testSelectionByLastTouch() {
    390  const mockSender = new MockDAPSender();
    391  const mockDateProvider = new MockDateProvider();
    392  const privateAttribution = new NewTabAttributionServiceClass({
    393    dapSender: mockSender,
    394    dateProvider: mockDateProvider,
    395  });
    396 
    397  const partnerIdentifier = "partner_identifier_last_touch";
    398  const viewIndex = 1;
    399  const clickIndex = 2;
    400 
    401  // Click at clickIndex
    402  await privateAttribution.onAttributionEvent("click", {
    403    partner_id: partnerIdentifier,
    404    index: clickIndex,
    405  });
    406 
    407  // step forward time so the view event occurs most recently
    408  mockDateProvider.add(10);
    409 
    410  // View at viewIndex
    411  await privateAttribution.onAttributionEvent("view", {
    412    partner_id: partnerIdentifier,
    413    index: viewIndex,
    414  });
    415 
    416  // Conversion filtering for "default" finds the view event
    417  await privateAttribution.onAttributionConversion(
    418    partnerIdentifier,
    419    LOOKBACK_DAYS,
    420    "default"
    421  );
    422 
    423  let receivedMeasurement = mockSender.receivedMeasurements.pop();
    424  Assert.deepEqual(receivedMeasurement.measurement, viewIndex);
    425  Assert.equal(mockSender.receivedMeasurements.length, 0);
    426 });
    427 
    428 add_task(async function testSelectionByPartnerId() {
    429  const mockSender = new MockDAPSender();
    430  const mockDateProvider = new MockDateProvider();
    431  const privateAttribution = new NewTabAttributionServiceClass({
    432    dapSender: mockSender,
    433    dateProvider: mockDateProvider,
    434  });
    435 
    436  const partnerIdentifier1 = "partner_identifier_1";
    437  const partnerIdentifier2 = "partner_identifier_2";
    438  const partner1Index = 1;
    439  const partner2Index = 2;
    440 
    441  // view event associated with partner 1
    442  await privateAttribution.onAttributionEvent("view", {
    443    partner_id: partnerIdentifier1,
    444    index: partner1Index,
    445  });
    446 
    447  // step forward time so the partner 2 event occurs most recently
    448  mockDateProvider.add(10);
    449 
    450  // view event associated with partner 2
    451  await privateAttribution.onAttributionEvent("view", {
    452    partner_id: partnerIdentifier2,
    453    index: partner2Index,
    454  });
    455 
    456  // Conversion filtering for "default" finds the correct view event
    457  await privateAttribution.onAttributionConversion(
    458    partnerIdentifier1,
    459    LOOKBACK_DAYS,
    460    "default"
    461  );
    462 
    463  let receivedMeasurement = mockSender.receivedMeasurements.pop();
    464  Assert.deepEqual(receivedMeasurement.measurement, partner1Index);
    465  Assert.equal(mockSender.receivedMeasurements.length, 0);
    466 });
    467 
    468 add_task(async function testExpiredImpressions() {
    469  const mockSender = new MockDAPSender();
    470  const mockDateProvider = new MockDateProvider();
    471  const privateAttribution = new NewTabAttributionServiceClass({
    472    dapSender: mockSender,
    473    dateProvider: mockDateProvider,
    474  });
    475 
    476  const partnerIdentifier = "partner_identifier";
    477  const index = 1;
    478  const defaultMeasurement = 0;
    479 
    480  // Register impression
    481  await privateAttribution.onAttributionEvent("view", {
    482    partner_id: partnerIdentifier,
    483    index,
    484  });
    485 
    486  // Fast-forward time by LOOKBACK_DAYS days + 1 ms
    487  mockDateProvider.add(LOOKBACK_DAYS * DAY_IN_MILLI + 1);
    488 
    489  await privateAttribution.onAttributionConversion(
    490    partnerIdentifier,
    491    LOOKBACK_DAYS,
    492    "view"
    493  );
    494 
    495  const receivedMeasurement = mockSender.receivedMeasurements.pop();
    496  Assert.deepEqual(receivedMeasurement.measurement, defaultMeasurement);
    497  Assert.equal(mockSender.receivedMeasurements.length, 0);
    498 });
    499 
    500 add_task(async function testConversionBudget() {
    501  const mockSender = new MockDAPSender();
    502  const privateAttribution = new NewTabAttributionServiceClass({
    503    dapSender: mockSender,
    504  });
    505 
    506  const partnerIdentifier = "partner_identifier_budget";
    507  const index = 1;
    508  const defaultMeasurement = 0;
    509 
    510  await privateAttribution.onAttributionEvent("view", {
    511    partner_id: partnerIdentifier,
    512    index,
    513  });
    514 
    515  // Measurements uploaded for conversions up to MAX_CONVERSIONS
    516  for (let i = 0; i < MAX_CONVERSIONS; i++) {
    517    await privateAttribution.onAttributionConversion(
    518      partnerIdentifier,
    519      LOOKBACK_DAYS,
    520      "view"
    521    );
    522 
    523    const receivedMeasurement = mockSender.receivedMeasurements.pop();
    524    Assert.deepEqual(receivedMeasurement.measurement, index);
    525    Assert.equal(mockSender.receivedMeasurements.length, 0);
    526  }
    527 
    528  // default report uploaded on subsequent conversions
    529  await privateAttribution.onAttributionConversion(
    530    partnerIdentifier,
    531    LOOKBACK_DAYS,
    532    "view"
    533  );
    534 
    535  const receivedMeasurement = mockSender.receivedMeasurements.pop();
    536  Assert.deepEqual(receivedMeasurement.measurement, defaultMeasurement);
    537  Assert.equal(mockSender.receivedMeasurements.length, 0);
    538 });
    539 
    540 add_task(async function testHistogramSize() {
    541  const mockSender = new MockDAPSender();
    542  const privateAttribution = new NewTabAttributionServiceClass({
    543    dapSender: mockSender,
    544  });
    545 
    546  const partnerIdentifier = "partner_identifier_bad_settings";
    547  const defaultMeasurement = 0;
    548  // Zero-based index equal to histogram size is out of bounds
    549  const index = HISTOGRAM_SIZE;
    550 
    551  await privateAttribution.onAttributionEvent("view", {
    552    partner_id: partnerIdentifier,
    553    index,
    554  });
    555 
    556  await privateAttribution.onAttributionConversion(
    557    partnerIdentifier,
    558    LOOKBACK_DAYS,
    559    "view"
    560  );
    561 
    562  const receivedMeasurement = mockSender.receivedMeasurements.pop();
    563  Assert.deepEqual(receivedMeasurement.measurement, defaultMeasurement);
    564  Assert.equal(mockSender.receivedMeasurements.length, 0);
    565 });
    566 
    567 add_task(async function testWithRealDAPSender() {
    568  // Omit mocking DAP telemetry sender in this test to defend against mock
    569  // sender getting out of sync
    570  Services.prefs.setStringPref("dap.ohttp.hpke", "");
    571  Services.prefs.setStringPref("dap.ohttp.relayURL", "");
    572  const mockServer = new MockServer();
    573  mockServer.start();
    574 
    575  const privateAttribution = new NewTabAttributionServiceClass();
    576 
    577  const partnerIdentifier = "partner_identifier_real_dap";
    578  const index = 1;
    579 
    580  await privateAttribution.onAttributionEvent("view", {
    581    partner_id: partnerIdentifier,
    582    index,
    583  });
    584 
    585  await privateAttribution.onAttributionConversion(
    586    partnerIdentifier,
    587    LOOKBACK_DAYS,
    588    "view"
    589  );
    590 
    591  await mockServer.stop();
    592 
    593  Assert.equal(mockServer.receivedReports.length, 1);
    594 
    595  const expectedReport = {
    596    contentType: "application/dap-report",
    597    size: 502,
    598  };
    599 
    600  const receivedReport = mockServer.receivedReports.pop();
    601  Assert.deepEqual(receivedReport, expectedReport);
    602 
    603  Services.prefs.clearUserPref("dap.ohttp.hpke");
    604  Services.prefs.clearUserPref("dap.ohttp.relayURL");
    605 });