tor-browser

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

test_remote_settings_poll.js (39908B)


      1 const { pushBroadcastService } = ChromeUtils.importESModule(
      2  "resource://gre/modules/PushBroadcastService.sys.mjs"
      3 );
      4 
      5 const { remoteSettingsBroadcastHandler, BROADCAST_ID } =
      6  ChromeUtils.importESModule(
      7    "resource://services-settings/remote-settings.sys.mjs"
      8  );
      9 
     10 const IS_ANDROID = AppConstants.platform == "android";
     11 
     12 const PREF_SETTINGS_SERVER = "services.settings.server";
     13 const PREF_SETTINGS_SERVER_BACKOFF = "services.settings.server.backoff";
     14 const PREF_LAST_UPDATE = "services.settings.last_update_seconds";
     15 const PREF_LAST_ETAG = "services.settings.last_etag";
     16 const PREF_CLOCK_SKEW_SECONDS = "services.settings.clock_skew_seconds";
     17 
     18 // Telemetry report result.
     19 const TELEMETRY_COMPONENT = "remotesettings";
     20 const TELEMETRY_SOURCE_POLL = "settings-changes-monitoring";
     21 const TELEMETRY_SOURCE_SYNC = "settings-sync";
     22 const CHANGES_PATH = "/v1" + Utils.CHANGES_PATH;
     23 
     24 var server;
     25 
     26 async function clear_state() {
     27  // set up prefs so the kinto updater talks to the test server
     28  Services.prefs.setStringPref(
     29    PREF_SETTINGS_SERVER,
     30    `http://localhost:${server.identity.primaryPort}/v1`
     31  );
     32 
     33  // set some initial values so we can check these are updated appropriately
     34  Services.prefs.setIntPref(PREF_LAST_UPDATE, 0);
     35  Services.prefs.setIntPref(PREF_CLOCK_SKEW_SECONDS, 0);
     36  Services.prefs.clearUserPref(PREF_LAST_ETAG);
     37 
     38  // Clear events snapshot.
     39  TelemetryTestUtils.assertEvents([], {}, { process: "dummy" });
     40 
     41  // Clear sync history.
     42  await new SyncHistory("").clear();
     43 }
     44 
     45 function serveChangesEntries(serverTime, entriesOrFunc) {
     46  return (request, response) => {
     47    response.setStatusLine(null, 200, "OK");
     48    response.setHeader("Content-Type", "application/json; charset=UTF-8");
     49    response.setHeader("Date", new Date(serverTime).toUTCString());
     50    const entries =
     51      typeof entriesOrFunc == "function" ? entriesOrFunc() : entriesOrFunc;
     52    const latest = entries[0]?.last_modified ?? 42;
     53    if (entries.length) {
     54      response.setHeader("ETag", `"${latest}"`);
     55    }
     56    response.write(JSON.stringify({ timestamp: latest, changes: entries }));
     57  };
     58 }
     59 
     60 add_setup(() => {
     61  // Set up an HTTP Server
     62  server = new HttpServer();
     63  server.start(-1);
     64 
     65  registerCleanupFunction(() => {
     66    server.stop(() => {});
     67  });
     68 });
     69 
     70 add_task(clear_state);
     71 
     72 add_task(async function test_an_event_is_sent_on_start() {
     73  server.registerPathHandler(CHANGES_PATH, (request, response) => {
     74    response.write(JSON.stringify({ timestamp: 42, changes: [] }));
     75    response.setHeader("Content-Type", "application/json; charset=UTF-8");
     76    response.setHeader("ETag", '"42"');
     77    response.setHeader("Date", new Date().toUTCString());
     78    response.setStatusLine(null, 200, "OK");
     79  });
     80  let notificationObserved = null;
     81  const observer = {
     82    observe(aSubject, aTopic, aData) {
     83      Services.obs.removeObserver(this, "remote-settings:changes-poll-start");
     84      notificationObserved = JSON.parse(aData);
     85    },
     86  };
     87  Services.obs.addObserver(observer, "remote-settings:changes-poll-start");
     88 
     89  await RemoteSettings.pollChanges({ expectedTimestamp: 13 });
     90 
     91  Assert.equal(
     92    notificationObserved.expectedTimestamp,
     93    13,
     94    "start notification should have been observed"
     95  );
     96 });
     97 add_task(clear_state);
     98 
     99 add_task(async function test_offline_is_reported_if_relevant() {
    100  const startSnapshot = getUptakeTelemetrySnapshot(
    101    TELEMETRY_COMPONENT,
    102    TELEMETRY_SOURCE_POLL
    103  );
    104  const offlineBackup = Services.io.offline;
    105  try {
    106    Services.io.offline = true;
    107 
    108    await RemoteSettings.pollChanges();
    109 
    110    const endSnapshot = getUptakeTelemetrySnapshot(
    111      TELEMETRY_COMPONENT,
    112      TELEMETRY_SOURCE_POLL
    113    );
    114    const expectedIncrements = {
    115      [UptakeTelemetry.STATUS.NETWORK_OFFLINE_ERROR]: 1,
    116    };
    117    checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
    118  } finally {
    119    Services.io.offline = offlineBackup;
    120  }
    121 });
    122 add_task(clear_state);
    123 
    124 add_task(async function test_check_success() {
    125  const serverTime = 8000;
    126 
    127  server.registerPathHandler(
    128    CHANGES_PATH,
    129    serveChangesEntries(serverTime, [
    130      {
    131        id: "330a0c5f-fadf-ff0b-40c8-4eb0d924ff6a",
    132        last_modified: 1100,
    133        host: "localhost",
    134        bucket: "some-other-bucket",
    135        collection: "test-collection",
    136      },
    137      {
    138        id: "254cbb9e-6888-4d9f-8e60-58b74faa8778",
    139        last_modified: 1000,
    140        host: "localhost",
    141        bucket: "test-bucket",
    142        collection: "test-collection",
    143      },
    144    ])
    145  );
    146 
    147  // add a test kinto client that will respond to lastModified information
    148  // for a collection called 'test-collection'.
    149  // Let's use a bucket that is not the default one (`test-bucket`).
    150  const c = RemoteSettings("test-collection", {
    151    bucketName: "test-bucket",
    152  });
    153  let maybeSyncCalled = false;
    154  c.maybeSync = () => {
    155    maybeSyncCalled = true;
    156  };
    157 
    158  // Ensure that the remote-settings:changes-poll-end notification works
    159  let notificationObserved = false;
    160  const observer = {
    161    observe() {
    162      Services.obs.removeObserver(this, "remote-settings:changes-poll-end");
    163      notificationObserved = true;
    164    },
    165  };
    166  Services.obs.addObserver(observer, "remote-settings:changes-poll-end");
    167 
    168  await RemoteSettings.pollChanges();
    169 
    170  // It didn't fail, hence we are sure that the unknown collection ``some-other-bucket/test-collection``
    171  // was ignored, otherwise it would have tried to reach the network.
    172 
    173  Assert.ok(maybeSyncCalled, "maybeSync was called");
    174  Assert.ok(notificationObserved, "a notification should have been observed");
    175  // Last timestamp was saved. An ETag header value is a quoted string.
    176  Assert.equal(Services.prefs.getStringPref(PREF_LAST_ETAG), '"1100"');
    177  // check the last_update is updated
    178  Assert.equal(Services.prefs.getIntPref(PREF_LAST_UPDATE), serverTime / 1000);
    179 
    180  // ensure that we've accumulated the correct telemetry
    181  TelemetryTestUtils.assertEvents(
    182    [
    183      [
    184        "uptake.remotecontent.result",
    185        "uptake",
    186        "remotesettings",
    187        UptakeTelemetry.STATUS.SUCCESS,
    188        {
    189          source: TELEMETRY_SOURCE_POLL,
    190          trigger: "manual",
    191        },
    192      ],
    193      [
    194        "uptake.remotecontent.result",
    195        "uptake",
    196        "remotesettings",
    197        UptakeTelemetry.STATUS.SUCCESS,
    198        {
    199          source: TELEMETRY_SOURCE_SYNC,
    200          trigger: "manual",
    201        },
    202      ],
    203    ],
    204    TELEMETRY_EVENTS_FILTERS
    205  );
    206 });
    207 add_task(clear_state);
    208 
    209 add_task(async function test_update_timer_interface() {
    210  const remoteSettings = Cc["@mozilla.org/services/settings;1"].getService(
    211    Ci.nsITimerCallback
    212  );
    213 
    214  const serverTime = 8000;
    215  server.registerPathHandler(
    216    CHANGES_PATH,
    217    serveChangesEntries(serverTime, [
    218      {
    219        id: "028261ad-16d4-40c2-a96a-66f72914d125",
    220        last_modified: 42,
    221        host: "localhost",
    222        bucket: "main",
    223        collection: "whatever-collection",
    224      },
    225    ])
    226  );
    227 
    228  await new Promise(resolve => {
    229    const e = "remote-settings:changes-poll-end";
    230    const changesPolledObserver = {
    231      observe() {
    232        Services.obs.removeObserver(this, e);
    233        resolve();
    234      },
    235    };
    236    Services.obs.addObserver(changesPolledObserver, e);
    237    remoteSettings.notify(null);
    238  });
    239 
    240  // Everything went fine.
    241  Assert.equal(Services.prefs.getStringPref(PREF_LAST_ETAG), '"42"');
    242  Assert.equal(Services.prefs.getIntPref(PREF_LAST_UPDATE), serverTime / 1000);
    243 });
    244 add_task(clear_state);
    245 
    246 add_task(async function test_check_up_to_date() {
    247  // Simulate a poll with up-to-date collection.
    248  const startSnapshot = getUptakeTelemetrySnapshot(
    249    TELEMETRY_COMPONENT,
    250    TELEMETRY_SOURCE_POLL
    251  );
    252 
    253  const serverTime = 4000;
    254  server.registerPathHandler(CHANGES_PATH, serveChangesEntries(serverTime, []));
    255 
    256  Services.prefs.setStringPref(PREF_LAST_ETAG, '"1100"');
    257 
    258  // Ensure that the remote-settings:changes-poll-end notification is sent.
    259  let notificationObserved = false;
    260  const observer = {
    261    observe() {
    262      Services.obs.removeObserver(this, "remote-settings:changes-poll-end");
    263      notificationObserved = true;
    264    },
    265  };
    266  Services.obs.addObserver(observer, "remote-settings:changes-poll-end");
    267 
    268  // If server has no change, maybeSync() is not called.
    269  let maybeSyncCalled = false;
    270  const c = RemoteSettings("test-collection", {
    271    bucketName: "test-bucket",
    272  });
    273  c.maybeSync = () => {
    274    maybeSyncCalled = true;
    275  };
    276 
    277  await RemoteSettings.pollChanges();
    278 
    279  Assert.ok(notificationObserved, "a notification should have been observed");
    280  Assert.ok(!maybeSyncCalled, "maybeSync should not be called");
    281  // Last update is overwritten
    282  Assert.equal(Services.prefs.getIntPref(PREF_LAST_UPDATE), serverTime / 1000);
    283 
    284  // ensure that we've accumulated the correct telemetry
    285  const endSnapshot = getUptakeTelemetrySnapshot(
    286    TELEMETRY_COMPONENT,
    287    TELEMETRY_SOURCE_POLL
    288  );
    289  const expectedIncrements = {
    290    [UptakeTelemetry.STATUS.UP_TO_DATE]: 1,
    291  };
    292  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
    293 });
    294 add_task(clear_state);
    295 
    296 add_task(async function test_expected_timestamp() {
    297  function withCacheBust(request, response) {
    298    const entries = [
    299      {
    300        id: "695c2407-de79-4408-91c7-70720dd59d78",
    301        last_modified: 1100,
    302        host: "localhost",
    303        bucket: "main",
    304        collection: "with-cache-busting",
    305      },
    306    ];
    307    if (
    308      request.queryString.includes(`_expected=${encodeURIComponent('"42"')}`)
    309    ) {
    310      response.write(
    311        JSON.stringify({
    312          timestamp: 1110,
    313          changes: entries,
    314        })
    315      );
    316    }
    317    response.setHeader("Content-Type", "application/json; charset=UTF-8");
    318    response.setHeader("ETag", '"1100"');
    319    response.setHeader("Date", new Date().toUTCString());
    320    response.setStatusLine(null, 200, "OK");
    321  }
    322  server.registerPathHandler(CHANGES_PATH, withCacheBust);
    323 
    324  const c = RemoteSettings("with-cache-busting");
    325  let maybeSyncCalled = false;
    326  c.maybeSync = () => {
    327    maybeSyncCalled = true;
    328  };
    329 
    330  await RemoteSettings.pollChanges({ expectedTimestamp: '"42"' });
    331 
    332  Assert.ok(maybeSyncCalled, "maybeSync was called");
    333 });
    334 add_task(clear_state);
    335 
    336 add_task(async function test_client_last_check_is_saved() {
    337  server.registerPathHandler(CHANGES_PATH, (request, response) => {
    338    response.write(
    339      JSON.stringify({
    340        timestamp: 42,
    341        changes: [
    342          {
    343            id: "695c2407-de79-4408-91c7-70720dd59d78",
    344            last_modified: 1100,
    345            host: "localhost",
    346            bucket: "main",
    347            collection: "models-recipes",
    348          },
    349        ],
    350      })
    351    );
    352    response.setHeader("Content-Type", "application/json; charset=UTF-8");
    353    response.setHeader("ETag", '"42"');
    354    response.setHeader("Date", new Date().toUTCString());
    355    response.setStatusLine(null, 200, "OK");
    356  });
    357 
    358  const c = RemoteSettings("models-recipes");
    359  c.maybeSync = () => {};
    360 
    361  equal(
    362    c.lastCheckTimePref,
    363    "services.settings.main.models-recipes.last_check"
    364  );
    365  Services.prefs.setIntPref(c.lastCheckTimePref, 0);
    366 
    367  await RemoteSettings.pollChanges({ expectedTimestamp: '"42"' });
    368 
    369  notEqual(Services.prefs.getIntPref(c.lastCheckTimePref), 0);
    370 });
    371 add_task(clear_state);
    372 
    373 const TELEMETRY_EVENTS_FILTERS = {
    374  category: "uptake.remotecontent.result",
    375  method: "uptake",
    376 };
    377 add_task(async function test_age_of_data_is_reported_in_uptake_status() {
    378  const serverTime = 1552323900000;
    379  const recordsTimestamp = serverTime - 3600 * 1000;
    380  server.registerPathHandler(
    381    CHANGES_PATH,
    382    serveChangesEntries(serverTime, [
    383      {
    384        id: "b6ba7fab-a40a-4d03-a4af-6b627f3c5b36",
    385        last_modified: recordsTimestamp,
    386        host: "localhost",
    387        bucket: "main",
    388        collection: "some-entry",
    389      },
    390    ])
    391  );
    392 
    393  await RemoteSettings.pollChanges();
    394 
    395  TelemetryTestUtils.assertEvents(
    396    [
    397      [
    398        "uptake.remotecontent.result",
    399        "uptake",
    400        "remotesettings",
    401        UptakeTelemetry.STATUS.SUCCESS,
    402        {
    403          source: TELEMETRY_SOURCE_POLL,
    404          age: "3600",
    405          trigger: "manual",
    406        },
    407      ],
    408      [
    409        "uptake.remotecontent.result",
    410        "uptake",
    411        "remotesettings",
    412        UptakeTelemetry.STATUS.SUCCESS,
    413        {
    414          source: TELEMETRY_SOURCE_SYNC,
    415          duration: () => true,
    416          trigger: "manual",
    417          timestamp: `"${recordsTimestamp}"`,
    418        },
    419      ],
    420    ],
    421    TELEMETRY_EVENTS_FILTERS
    422  );
    423 });
    424 add_task(clear_state);
    425 
    426 add_task(
    427  async function test_synchronization_duration_is_reported_in_uptake_status() {
    428    server.registerPathHandler(
    429      CHANGES_PATH,
    430      serveChangesEntries(10000, [
    431        {
    432          id: "b6ba7fab-a40a-4d03-a4af-6b627f3c5b36",
    433          last_modified: 42,
    434          host: "localhost",
    435          bucket: "main",
    436          collection: "some-entry",
    437        },
    438      ])
    439    );
    440    const c = RemoteSettings("some-entry");
    441    // Simulate a synchronization that lasts 1 sec.
    442    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    443    c.maybeSync = () => new Promise(resolve => setTimeout(resolve, 1000));
    444 
    445    await RemoteSettings.pollChanges();
    446 
    447    TelemetryTestUtils.assertEvents(
    448      [
    449        [
    450          "uptake.remotecontent.result",
    451          "uptake",
    452          "remotesettings",
    453          "success",
    454          {
    455            source: TELEMETRY_SOURCE_POLL,
    456            age: () => true,
    457            trigger: "manual",
    458          },
    459        ],
    460        [
    461          "uptake.remotecontent.result",
    462          "uptake",
    463          "remotesettings",
    464          "success",
    465          {
    466            source: TELEMETRY_SOURCE_SYNC,
    467            duration: v => v >= 1000,
    468            trigger: "manual",
    469          },
    470        ],
    471      ],
    472      TELEMETRY_EVENTS_FILTERS
    473    );
    474  }
    475 );
    476 add_task(clear_state);
    477 
    478 add_task(async function test_success_with_partial_list() {
    479  function partialList(request, response) {
    480    const entries = [
    481      {
    482        id: "028261ad-16d4-40c2-a96a-66f72914d125",
    483        last_modified: 43,
    484        host: "localhost",
    485        bucket: "main",
    486        collection: "cid-1",
    487      },
    488      {
    489        id: "98a34576-bcd6-423f-abc2-1d290b776ed8",
    490        last_modified: 42,
    491        host: "localhost",
    492        bucket: "main",
    493        collection: "poll-test-collection",
    494      },
    495    ];
    496    if (request.queryString.includes(`_since=${encodeURIComponent('"42"')}`)) {
    497      response.write(
    498        JSON.stringify({
    499          timestamp: 43,
    500          changes: entries.slice(0, 1),
    501        })
    502      );
    503    } else {
    504      response.write(
    505        JSON.stringify({
    506          timestamp: 42,
    507          changes: entries,
    508        })
    509      );
    510    }
    511    response.setHeader("Content-Type", "application/json; charset=UTF-8");
    512    response.setHeader("Date", new Date().toUTCString());
    513    response.setStatusLine(null, 200, "OK");
    514  }
    515  server.registerPathHandler(CHANGES_PATH, partialList);
    516 
    517  const c = RemoteSettings("poll-test-collection");
    518  let maybeSyncCount = 0;
    519  c.maybeSync = () => {
    520    maybeSyncCount++;
    521  };
    522 
    523  await RemoteSettings.pollChanges();
    524  await RemoteSettings.pollChanges();
    525 
    526  // On the second call, the server does not mention the poll-test-collection
    527  // and maybeSync() is not called.
    528  Assert.equal(maybeSyncCount, 1, "maybeSync should not be called twice");
    529 });
    530 add_task(clear_state);
    531 
    532 add_task(async function test_full_polling() {
    533  server.registerPathHandler(
    534    CHANGES_PATH,
    535    serveChangesEntries(10000, [
    536      {
    537        id: "b6ba7fab-a40a-4d03-a4af-6b627f3c5b36",
    538        last_modified: 42,
    539        host: "localhost",
    540        bucket: "main",
    541        collection: "poll-test-collection",
    542      },
    543    ])
    544  );
    545 
    546  const c = RemoteSettings("poll-test-collection");
    547  let maybeSyncCount = 0;
    548  c.maybeSync = () => {
    549    maybeSyncCount++;
    550  };
    551 
    552  await RemoteSettings.pollChanges();
    553  await RemoteSettings.pollChanges({ full: true });
    554 
    555  // Since the second call is full, clients are called
    556  Assert.equal(maybeSyncCount, 2, "maybeSync should be called twice");
    557 });
    558 add_task(clear_state);
    559 
    560 add_task(async function test_server_bad_json() {
    561  const startSnapshot = getUptakeTelemetrySnapshot(
    562    TELEMETRY_COMPONENT,
    563    TELEMETRY_SOURCE_POLL
    564  );
    565 
    566  function simulateBadJSON(request, response) {
    567    response.setHeader("Content-Type", "application/json; charset=UTF-8");
    568    response.write("<html></html>");
    569    response.setStatusLine(null, 200, "OK");
    570  }
    571  server.registerPathHandler(CHANGES_PATH, simulateBadJSON);
    572 
    573  let error;
    574  try {
    575    await RemoteSettings.pollChanges();
    576  } catch (e) {
    577    error = e;
    578  }
    579  Assert.ok(/JSON.parse: unexpected character/.test(error.message));
    580 
    581  const endSnapshot = getUptakeTelemetrySnapshot(
    582    TELEMETRY_COMPONENT,
    583    TELEMETRY_SOURCE_POLL
    584  );
    585  const expectedIncrements = {
    586    [UptakeTelemetry.STATUS.PARSE_ERROR]: 1,
    587  };
    588  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
    589 });
    590 add_task(clear_state);
    591 
    592 add_task(async function test_server_bad_content_type() {
    593  const startSnapshot = getUptakeTelemetrySnapshot(
    594    TELEMETRY_COMPONENT,
    595    TELEMETRY_SOURCE_POLL
    596  );
    597 
    598  function simulateBadContentType(request, response) {
    599    response.setHeader("Content-Type", "text/html");
    600    response.write("<html></html>");
    601    response.setStatusLine(null, 200, "OK");
    602  }
    603  server.registerPathHandler(CHANGES_PATH, simulateBadContentType);
    604 
    605  let error;
    606  try {
    607    await RemoteSettings.pollChanges();
    608  } catch (e) {
    609    error = e;
    610  }
    611  Assert.ok(/Unexpected content-type/.test(error.message));
    612 
    613  const endSnapshot = getUptakeTelemetrySnapshot(
    614    TELEMETRY_COMPONENT,
    615    TELEMETRY_SOURCE_POLL
    616  );
    617  const expectedIncrements = {
    618    [UptakeTelemetry.STATUS.CONTENT_ERROR]: 1,
    619  };
    620  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
    621 });
    622 add_task(clear_state);
    623 
    624 add_task(async function test_server_404_response() {
    625  function simulateDummy404(request, response) {
    626    response.setHeader("Content-Type", "text/html; charset=UTF-8");
    627    response.write("<html></html>");
    628    response.setStatusLine(null, 404, "OK");
    629  }
    630  server.registerPathHandler(CHANGES_PATH, simulateDummy404);
    631 
    632  await RemoteSettings.pollChanges(); // Does not fail when running from tests.
    633 });
    634 add_task(clear_state);
    635 
    636 add_task(async function test_server_error() {
    637  const startSnapshot = getUptakeTelemetrySnapshot(
    638    TELEMETRY_COMPONENT,
    639    TELEMETRY_SOURCE_POLL
    640  );
    641 
    642  // Simulate a server error.
    643  function simulateErrorResponse(request, response) {
    644    response.setHeader("Date", new Date(3000).toUTCString());
    645    response.setHeader("Content-Type", "application/json; charset=UTF-8");
    646    response.write(
    647      JSON.stringify({
    648        code: 503,
    649        errno: 999,
    650        error: "Service Unavailable",
    651      })
    652    );
    653    response.setStatusLine(null, 503, "Service Unavailable");
    654  }
    655  server.registerPathHandler(CHANGES_PATH, simulateErrorResponse);
    656 
    657  let notificationObserved = false;
    658  const observer = {
    659    observe() {
    660      Services.obs.removeObserver(this, "remote-settings:changes-poll-end");
    661      notificationObserved = true;
    662    },
    663  };
    664  Services.obs.addObserver(observer, "remote-settings:changes-poll-end");
    665  Services.prefs.setIntPref(PREF_LAST_UPDATE, 42);
    666 
    667  // pollChanges() fails with adequate error and no notification.
    668  let error;
    669  try {
    670    await RemoteSettings.pollChanges();
    671  } catch (e) {
    672    error = e;
    673  }
    674 
    675  Assert.ok(
    676    !notificationObserved,
    677    "a notification should not have been observed"
    678  );
    679  Assert.ok(/Polling for changes failed/.test(error.message));
    680  // When an error occurs, last update was not overwritten.
    681  Assert.equal(Services.prefs.getIntPref(PREF_LAST_UPDATE), 42);
    682  // ensure that we've accumulated the correct telemetry
    683  const endSnapshot = getUptakeTelemetrySnapshot(
    684    TELEMETRY_COMPONENT,
    685    TELEMETRY_SOURCE_POLL
    686  );
    687  const expectedIncrements = {
    688    [UptakeTelemetry.STATUS.SERVER_ERROR]: 1,
    689  };
    690  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
    691 });
    692 add_task(clear_state);
    693 
    694 add_task(async function test_server_error_5xx() {
    695  const startSnapshot = getUptakeTelemetrySnapshot(
    696    TELEMETRY_COMPONENT,
    697    TELEMETRY_SOURCE_POLL
    698  );
    699 
    700  function simulateErrorResponse(request, response) {
    701    response.setHeader("Date", new Date(3000).toUTCString());
    702    response.setHeader("Content-Type", "text/html; charset=UTF-8");
    703    response.write("<html></html>");
    704    response.setStatusLine(null, 504, "Gateway Timeout");
    705  }
    706  server.registerPathHandler(CHANGES_PATH, simulateErrorResponse);
    707 
    708  try {
    709    await RemoteSettings.pollChanges();
    710  } catch (e) {}
    711 
    712  const endSnapshot = getUptakeTelemetrySnapshot(
    713    TELEMETRY_COMPONENT,
    714    TELEMETRY_SOURCE_POLL
    715  );
    716  const expectedIncrements = {
    717    [UptakeTelemetry.STATUS.SERVER_ERROR]: 1,
    718  };
    719  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
    720 });
    721 add_task(clear_state);
    722 
    723 add_task(async function test_server_error_4xx() {
    724  function simulateErrorResponse(request, response) {
    725    response.setHeader("Date", new Date(3000).toUTCString());
    726    response.setHeader("Content-Type", "application/json; charset=UTF-8");
    727    if (request.queryString.includes(`_since=${encodeURIComponent('"abc"')}`)) {
    728      response.setStatusLine(null, 400, "Bad Request");
    729      response.write(JSON.stringify({}));
    730    } else {
    731      response.setStatusLine(null, 200, "OK");
    732      response.write(JSON.stringify({ changes: [] }));
    733    }
    734  }
    735  server.registerPathHandler(CHANGES_PATH, simulateErrorResponse);
    736 
    737  Services.prefs.setStringPref(PREF_LAST_ETAG, '"abc"');
    738 
    739  let error;
    740  try {
    741    await RemoteSettings.pollChanges();
    742  } catch (e) {
    743    error = e;
    744  }
    745 
    746  Assert.ok(error.message.includes("400 Bad Request"), "Polling failed");
    747  Assert.ok(
    748    !Services.prefs.prefHasUserValue(PREF_LAST_ETAG),
    749    "Last ETag pref was cleared"
    750  );
    751 
    752  await RemoteSettings.pollChanges(); // Does not raise.
    753 });
    754 add_task(clear_state);
    755 
    756 add_task(async function test_client_error() {
    757  const startSnapshot = getUptakeTelemetrySnapshot(
    758    TELEMETRY_COMPONENT,
    759    TELEMETRY_SOURCE_SYNC
    760  );
    761 
    762  const collectionDetails = {
    763    id: "b6ba7fab-a40a-4d03-a4af-6b627f3c5b36",
    764    last_modified: 42,
    765    host: "localhost",
    766    bucket: "main",
    767    collection: "some-entry",
    768  };
    769  server.registerPathHandler(
    770    CHANGES_PATH,
    771    serveChangesEntries(10000, [collectionDetails])
    772  );
    773  const c = RemoteSettings("some-entry");
    774  c.maybeSync = () => {
    775    throw new RemoteSettingsClient.CorruptedDataError("main/some-entry");
    776  };
    777 
    778  let notificationsObserved = [];
    779  const observer = {
    780    observe(aSubject, aTopic) {
    781      Services.obs.removeObserver(this, aTopic);
    782      notificationsObserved.push([aTopic, aSubject.wrappedJSObject]);
    783    },
    784  };
    785  Services.obs.addObserver(observer, "remote-settings:changes-poll-end");
    786  Services.obs.addObserver(observer, "remote-settings:sync-error");
    787  Services.prefs.setIntPref(PREF_LAST_ETAG, 42);
    788 
    789  // pollChanges() fails with adequate error and a sync-error notification.
    790  let error;
    791  try {
    792    await RemoteSettings.pollChanges();
    793  } catch (e) {
    794    error = e;
    795  }
    796 
    797  Assert.equal(
    798    notificationsObserved.length,
    799    1,
    800    "only the error notification should not have been observed"
    801  );
    802  console.log(notificationsObserved);
    803  let [topicObserved, subjectObserved] = notificationsObserved[0];
    804  Assert.equal(topicObserved, "remote-settings:sync-error");
    805  Assert.ok(
    806    subjectObserved.error instanceof RemoteSettingsClient.CorruptedDataError,
    807    `original error is provided (got ${subjectObserved.error})`
    808  );
    809  Assert.deepEqual(
    810    subjectObserved.error.details,
    811    collectionDetails,
    812    "information about collection is provided"
    813  );
    814 
    815  Assert.ok(/Corrupted/.test(error.message), "original client error is thrown");
    816  // When an error occurs, last etag was not overwritten.
    817  Assert.equal(Services.prefs.getIntPref(PREF_LAST_ETAG), 42);
    818  // ensure that we've accumulated the correct telemetry
    819  const endSnapshot = getUptakeTelemetrySnapshot(
    820    TELEMETRY_COMPONENT,
    821    TELEMETRY_SOURCE_SYNC
    822  );
    823  const expectedIncrements = {
    824    [UptakeTelemetry.STATUS.SYNC_ERROR]: 1,
    825  };
    826  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
    827 });
    828 add_task(clear_state);
    829 
    830 add_task(async function test_sync_success_is_stored_in_history() {
    831  const collectionDetails = {
    832    last_modified: 444,
    833    bucket: "main",
    834    collection: "desktop-manager",
    835  };
    836  server.registerPathHandler(
    837    CHANGES_PATH,
    838    serveChangesEntries(10000, [collectionDetails])
    839  );
    840  const c = RemoteSettings("desktop-manager");
    841  c.maybeSync = () => {};
    842  try {
    843    await RemoteSettings.pollChanges({ expectedTimestamp: 555 });
    844  } catch (e) {}
    845 
    846  const { history } = await RemoteSettings.inspect();
    847 
    848  Assert.deepEqual(history, {
    849    [TELEMETRY_SOURCE_SYNC]: [
    850      {
    851        timestamp: 444,
    852        status: "success",
    853        infos: {},
    854        datetime: new Date(444),
    855      },
    856    ],
    857  });
    858 });
    859 add_task(clear_state);
    860 
    861 add_task(async function test_sync_error_is_stored_in_history() {
    862  const collectionDetails = {
    863    last_modified: 1337,
    864    bucket: "main",
    865    collection: "desktop-manager",
    866  };
    867  server.registerPathHandler(
    868    CHANGES_PATH,
    869    serveChangesEntries(10000, [collectionDetails])
    870  );
    871  const c = RemoteSettings("desktop-manager");
    872  c.maybeSync = () => {
    873    throw new RemoteSettingsClient.MissingSignatureError(
    874      "main/desktop-manager"
    875    );
    876  };
    877  try {
    878    await RemoteSettings.pollChanges({ expectedTimestamp: 123456 });
    879  } catch (e) {}
    880 
    881  const { history } = await RemoteSettings.inspect();
    882 
    883  Assert.deepEqual(history, {
    884    [TELEMETRY_SOURCE_SYNC]: [
    885      {
    886        timestamp: 1337,
    887        status: "sync_error",
    888        infos: {
    889          expectedTimestamp: 123456,
    890          errorName: "MissingSignatureError",
    891        },
    892        datetime: new Date(1337),
    893      },
    894    ],
    895  });
    896 });
    897 add_task(clear_state);
    898 
    899 add_task(
    900  async function test_sync_broken_signal_is_sent_on_consistent_failure() {
    901    const startSnapshot = getUptakeTelemetrySnapshot(
    902      TELEMETRY_COMPONENT,
    903      TELEMETRY_SOURCE_POLL
    904    );
    905    // Wait for the "sync-broken-error" notification.
    906    let notificationObserved = false;
    907    const observer = {
    908      observe() {
    909        notificationObserved = true;
    910      },
    911    };
    912    Services.obs.addObserver(observer, "remote-settings:broken-sync-error");
    913    // Register a client with a failing sync method.
    914    const c = RemoteSettings("desktop-manager");
    915    c.maybeSync = () => {
    916      throw new RemoteSettingsClient.InvalidSignatureError(
    917        "main/desktop-manager"
    918      );
    919    };
    920    // Simulate a response whose ETag gets incremented on each call
    921    // (in order to generate several history entries, indexed by timestamp).
    922    let timestamp = 1337;
    923    server.registerPathHandler(
    924      CHANGES_PATH,
    925      serveChangesEntries(10000, () => {
    926        return [
    927          {
    928            last_modified: ++timestamp,
    929            bucket: "main",
    930            collection: "desktop-manager",
    931          },
    932        ];
    933      })
    934    );
    935 
    936    // Now obtain several failures in a row (less than threshold).
    937    for (var i = 0; i < 9; i++) {
    938      try {
    939        await RemoteSettings.pollChanges();
    940      } catch (e) {}
    941    }
    942    Assert.ok(!notificationObserved, "Not notified yet");
    943 
    944    // Fail again once. Will now notify.
    945    try {
    946      await RemoteSettings.pollChanges();
    947    } catch (e) {}
    948    Assert.ok(notificationObserved, "Broken sync notified");
    949    // Uptake event to notify broken sync is sent.
    950    const endSnapshot = getUptakeTelemetrySnapshot(
    951      TELEMETRY_COMPONENT,
    952      TELEMETRY_SOURCE_SYNC
    953    );
    954    const expectedIncrements = {
    955      [UptakeTelemetry.STATUS.SYNC_ERROR]: 10,
    956      [UptakeTelemetry.STATUS.SYNC_BROKEN_ERROR]: 1,
    957    };
    958    checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
    959 
    960    // Synchronize successfully.
    961    notificationObserved = false;
    962    const failingSync = c.maybeSync;
    963    c.maybeSync = () => {};
    964    await RemoteSettings.pollChanges();
    965 
    966    const { history } = await RemoteSettings.inspect();
    967    Assert.equal(
    968      history[TELEMETRY_SOURCE_SYNC][0].status,
    969      UptakeTelemetry.STATUS.SUCCESS,
    970      "Last sync is success"
    971    );
    972    Assert.ok(!notificationObserved, "Not notified after success");
    973 
    974    // Now fail again. Broken sync isn't notified, we need several in a row.
    975    c.maybeSync = failingSync;
    976    try {
    977      await RemoteSettings.pollChanges();
    978    } catch (e) {}
    979    Assert.ok(!notificationObserved, "Not notified on single error");
    980    Services.obs.removeObserver(observer, "remote-settings:broken-sync-error");
    981  }
    982 );
    983 add_task(clear_state);
    984 
    985 add_task(async function test_check_clockskew_is_updated() {
    986  const serverTime = 2000;
    987 
    988  function serverResponse(request, response) {
    989    response.setHeader("Content-Type", "application/json; charset=UTF-8");
    990    response.setHeader("Date", new Date(serverTime).toUTCString());
    991    response.write(JSON.stringify({ timestamp: 42, changes: [] }));
    992    response.setStatusLine(null, 200, "OK");
    993  }
    994  server.registerPathHandler(CHANGES_PATH, serverResponse);
    995 
    996  let startTime = Date.now();
    997 
    998  await RemoteSettings.pollChanges();
    999 
   1000  // How does the clock difference look?
   1001  let endTime = Date.now();
   1002  let clockDifference = Services.prefs.getIntPref(PREF_CLOCK_SKEW_SECONDS);
   1003  // we previously set the serverTime to 2 (seconds past epoch)
   1004  Assert.ok(
   1005    clockDifference <= endTime / 1000 &&
   1006      clockDifference >= Math.floor(startTime / 1000) - serverTime / 1000
   1007  );
   1008 
   1009  // check negative clock skew times
   1010  // set to a time in the future
   1011  server.registerPathHandler(
   1012    CHANGES_PATH,
   1013    serveChangesEntries(Date.now() + 10000, [])
   1014  );
   1015 
   1016  await RemoteSettings.pollChanges();
   1017 
   1018  clockDifference = Services.prefs.getIntPref(PREF_CLOCK_SKEW_SECONDS);
   1019  // we previously set the serverTime to Date.now() + 10000 ms past epoch
   1020  Assert.ok(clockDifference <= 0 && clockDifference >= -10);
   1021 });
   1022 add_task(clear_state);
   1023 
   1024 add_task(async function test_check_clockskew_takes_age_into_account() {
   1025  const currentTime = Date.now();
   1026  const skewSeconds = 5;
   1027  const ageCDNSeconds = 3600;
   1028  const serverTime = currentTime - skewSeconds * 1000 - ageCDNSeconds * 1000;
   1029 
   1030  function serverResponse(request, response) {
   1031    response.setHeader("Content-Type", "application/json; charset=UTF-8");
   1032    response.setHeader("Date", new Date(serverTime).toUTCString());
   1033    response.setHeader("Age", `${ageCDNSeconds}`);
   1034    response.write(JSON.stringify({ timestamp: 42, changes: [] }));
   1035    response.setStatusLine(null, 200, "OK");
   1036  }
   1037  server.registerPathHandler(CHANGES_PATH, serverResponse);
   1038 
   1039  await RemoteSettings.pollChanges();
   1040 
   1041  const clockSkew = Services.prefs.getIntPref(PREF_CLOCK_SKEW_SECONDS);
   1042  Assert.greaterOrEqual(clockSkew, skewSeconds, `clockSkew is ${clockSkew}`);
   1043 });
   1044 add_task(clear_state);
   1045 
   1046 add_task(async function test_backoff() {
   1047  const startSnapshot = getUptakeTelemetrySnapshot(
   1048    TELEMETRY_COMPONENT,
   1049    TELEMETRY_SOURCE_POLL
   1050  );
   1051 
   1052  function simulateBackoffResponse(request, response) {
   1053    response.setHeader("Content-Type", "application/json; charset=UTF-8");
   1054    response.setHeader("Backoff", "10");
   1055    response.write(JSON.stringify({ timestamp: 42, changes: [] }));
   1056    response.setStatusLine(null, 200, "OK");
   1057  }
   1058  server.registerPathHandler(CHANGES_PATH, simulateBackoffResponse);
   1059 
   1060  // First will work.
   1061  await RemoteSettings.pollChanges();
   1062  // Second will fail because we haven't waited.
   1063  try {
   1064    await RemoteSettings.pollChanges();
   1065    // The previous line should have thrown an error.
   1066    Assert.ok(false);
   1067  } catch (e) {
   1068    Assert.ok(
   1069      /Server is asking clients to back off; retry in \d+s./.test(e.message)
   1070    );
   1071  }
   1072 
   1073  // Once backoff time has expired, polling for changes can start again.
   1074  server.registerPathHandler(
   1075    CHANGES_PATH,
   1076    serveChangesEntries(12000, [
   1077      {
   1078        id: "6a733d4a-601e-11e8-837a-0f85257529a1",
   1079        last_modified: 1300,
   1080        host: "localhost",
   1081        bucket: "some-bucket",
   1082        collection: "some-collection",
   1083      },
   1084    ])
   1085  );
   1086  Services.prefs.setStringPref(
   1087    PREF_SETTINGS_SERVER_BACKOFF,
   1088    `${Date.now() - 1000}`
   1089  );
   1090 
   1091  await RemoteSettings.pollChanges();
   1092 
   1093  // Backoff tracking preference was cleared.
   1094  Assert.ok(!Services.prefs.prefHasUserValue(PREF_SETTINGS_SERVER_BACKOFF));
   1095 
   1096  // Ensure that we've accumulated the correct telemetry
   1097  const endSnapshot = getUptakeTelemetrySnapshot(
   1098    TELEMETRY_COMPONENT,
   1099    TELEMETRY_SOURCE_POLL
   1100  );
   1101  const expectedIncrements = {
   1102    [UptakeTelemetry.STATUS.SUCCESS]: 1,
   1103    [UptakeTelemetry.STATUS.UP_TO_DATE]: 1,
   1104    [UptakeTelemetry.STATUS.BACKOFF]: 1,
   1105  };
   1106  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
   1107 });
   1108 add_task(clear_state);
   1109 
   1110 add_task(async function test_network_error() {
   1111  const startSnapshot = getUptakeTelemetrySnapshot(
   1112    TELEMETRY_COMPONENT,
   1113    TELEMETRY_SOURCE_POLL
   1114  );
   1115 
   1116  // Simulate a network error (to check telemetry report).
   1117  Services.prefs.setStringPref(PREF_SETTINGS_SERVER, "http://localhost:42/v1");
   1118  try {
   1119    await RemoteSettings.pollChanges();
   1120  } catch (e) {}
   1121 
   1122  // ensure that we've accumulated the correct telemetry
   1123  const endSnapshot = getUptakeTelemetrySnapshot(
   1124    TELEMETRY_COMPONENT,
   1125    TELEMETRY_SOURCE_POLL
   1126  );
   1127  const expectedIncrements = {
   1128    [UptakeTelemetry.STATUS.NETWORK_ERROR]: 1,
   1129  };
   1130  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
   1131 });
   1132 add_task(clear_state);
   1133 
   1134 add_task(async function test_syncs_clients_with_local_database() {
   1135  server.registerPathHandler(
   1136    CHANGES_PATH,
   1137    serveChangesEntries(42000, [
   1138      {
   1139        id: "d4a14f44-601f-11e8-8b8a-030f3dc5b844",
   1140        last_modified: 10000,
   1141        host: "localhost",
   1142        bucket: "main",
   1143        collection: "some-unknown",
   1144      },
   1145      {
   1146        id: "39f57e4e-6023-11e8-8b74-77c8dedfb389",
   1147        last_modified: 9000,
   1148        host: "localhost",
   1149        bucket: "blocklists",
   1150        collection: "addons",
   1151      },
   1152      {
   1153        id: "9a594c1a-601f-11e8-9c8a-33b2239d9113",
   1154        last_modified: 8000,
   1155        host: "localhost",
   1156        bucket: "main",
   1157        collection: "recipes",
   1158      },
   1159    ])
   1160  );
   1161 
   1162  // This simulates what remote-settings would do when initializing a local database.
   1163  // We don't want to instantiate a client using the RemoteSettings() API
   1164  // since we want to test «unknown» clients that have a local database.
   1165  new RemoteSettingsClient("addons", {
   1166    bucketName: "blocklists",
   1167  }).db.importChanges({}, 42);
   1168  new RemoteSettingsClient("recipes").db.importChanges({}, 43);
   1169 
   1170  let error;
   1171  try {
   1172    await RemoteSettings.pollChanges();
   1173    Assert.ok(false, "pollChange() should throw when pulling recipes");
   1174  } catch (e) {
   1175    error = e;
   1176  }
   1177 
   1178  // The `main/some-unknown` should be skipped because it has no local database.
   1179  // The `blocklists/addons` should be skipped because it is not the main bucket.
   1180  // The `recipes` has a local database, and should cause a network error because the test
   1181  // does not setup the server to receive the requests of `maybeSync()`.
   1182  Assert.ok(/HTTP 404/.test(error.message), "server will return 404 on sync");
   1183  Assert.equal(error.details.collection, "recipes");
   1184 });
   1185 add_task(clear_state);
   1186 
   1187 add_task(async function test_syncs_clients_with_local_dump() {
   1188  if (IS_ANDROID) {
   1189    // Skip test: we don't ship remote settings dumps on Android (see package-manifest).
   1190    return;
   1191  }
   1192  server.registerPathHandler(
   1193    CHANGES_PATH,
   1194    serveChangesEntries(42000, [
   1195      {
   1196        id: "d4a14f44-601f-11e8-8b8a-030f3dc5b844",
   1197        last_modified: 10000,
   1198        host: "localhost",
   1199        bucket: "main",
   1200        collection: "some-unknown",
   1201      },
   1202      {
   1203        id: "39f57e4e-6023-11e8-8b74-77c8dedfb389",
   1204        last_modified: 9000,
   1205        host: "localhost",
   1206        bucket: "blocklists",
   1207        collection: "addons",
   1208      },
   1209      {
   1210        id: "9a594c1a-601f-11e8-9c8a-33b2239d9113",
   1211        last_modified: 8000,
   1212        host: "localhost",
   1213        bucket: "main",
   1214        collection: "example",
   1215      },
   1216    ])
   1217  );
   1218 
   1219  let error;
   1220  try {
   1221    await RemoteSettings.pollChanges();
   1222  } catch (e) {
   1223    error = e;
   1224  }
   1225 
   1226  // The `main/some-unknown` should be skipped because it has no dump.
   1227  // The `blocklists/addons` should be skipped because it is not the main bucket.
   1228  // The `example` has a dump, and should cause a network error because the test
   1229  // does not setup the server to receive the requests of `maybeSync()`.
   1230  Assert.ok(/HTTP 404/.test(error.message), "server will return 404 on sync");
   1231  Assert.equal(error.details.collection, "example");
   1232 });
   1233 add_task(clear_state);
   1234 
   1235 add_task(async function test_adding_client_resets_polling() {
   1236  function serve200(request, response) {
   1237    const entries = [
   1238      {
   1239        id: "aa71e6cc-9f37-447a-b6e0-c025e8eabd03",
   1240        last_modified: 42,
   1241        host: "localhost",
   1242        bucket: "main",
   1243        collection: "a-collection",
   1244      },
   1245    ];
   1246    if (request.queryString.includes("_since")) {
   1247      response.write(
   1248        JSON.stringify({
   1249          timestamp: 42,
   1250          changes: [],
   1251        })
   1252      );
   1253    } else {
   1254      response.write(
   1255        JSON.stringify({
   1256          timestamp: 42,
   1257          changes: entries,
   1258        })
   1259      );
   1260    }
   1261    response.setStatusLine(null, 200, "OK");
   1262    response.setHeader("Content-Type", "application/json; charset=UTF-8");
   1263    response.setHeader("Date", new Date().toUTCString());
   1264  }
   1265  server.registerPathHandler(CHANGES_PATH, serve200);
   1266 
   1267  // Poll once, without any client for "a-collection"
   1268  await RemoteSettings.pollChanges();
   1269 
   1270  // Register a new client.
   1271  let maybeSyncCalled = false;
   1272  const c = RemoteSettings("a-collection");
   1273  c.maybeSync = () => {
   1274    maybeSyncCalled = true;
   1275  };
   1276 
   1277  // Poll again.
   1278  await RemoteSettings.pollChanges();
   1279 
   1280  // The new client was called, even if the server data didn't change.
   1281  Assert.ok(maybeSyncCalled);
   1282 
   1283  // Poll again. This time maybeSync() won't be called.
   1284  maybeSyncCalled = false;
   1285  await RemoteSettings.pollChanges();
   1286  Assert.ok(!maybeSyncCalled);
   1287 });
   1288 add_task(clear_state);
   1289 
   1290 add_task(
   1291  async function test_broadcast_handler_passes_version_and_trigger_values() {
   1292    // The polling will use the broadcast version as cache busting query param.
   1293    let passedQueryString;
   1294    function serveCacheBusted(request, response) {
   1295      passedQueryString = request.queryString;
   1296      const entries = [
   1297        {
   1298          id: "b6ba7fab-a40a-4d03-a4af-6b627f3c5b36",
   1299          last_modified: 42,
   1300          host: "localhost",
   1301          bucket: "main",
   1302          collection: "from-broadcast",
   1303        },
   1304      ];
   1305      response.write(
   1306        JSON.stringify({
   1307          changes: entries,
   1308          timestamp: 42,
   1309        })
   1310      );
   1311      response.setHeader("ETag", '"42"');
   1312      response.setStatusLine(null, 200, "OK");
   1313      response.setHeader("Content-Type", "application/json; charset=UTF-8");
   1314      response.setHeader("Date", new Date().toUTCString());
   1315    }
   1316    server.registerPathHandler(CHANGES_PATH, serveCacheBusted);
   1317 
   1318    let passedTrigger;
   1319    const c = RemoteSettings("from-broadcast");
   1320    c.maybeSync = (last_modified, { trigger }) => {
   1321      passedTrigger = trigger;
   1322    };
   1323 
   1324    const version = "1337";
   1325 
   1326    let context = { phase: pushBroadcastService.PHASES.HELLO };
   1327    await remoteSettingsBroadcastHandler.receivedBroadcastMessage(
   1328      version,
   1329      BROADCAST_ID,
   1330      context
   1331    );
   1332    Assert.equal(passedTrigger, "startup");
   1333    Assert.equal(passedQueryString, `_expected=${version}`);
   1334 
   1335    clear_state();
   1336 
   1337    context = { phase: pushBroadcastService.PHASES.REGISTER };
   1338    await remoteSettingsBroadcastHandler.receivedBroadcastMessage(
   1339      version,
   1340      BROADCAST_ID,
   1341      context
   1342    );
   1343    Assert.equal(passedTrigger, "startup");
   1344 
   1345    clear_state();
   1346 
   1347    context = { phase: pushBroadcastService.PHASES.BROADCAST };
   1348    await remoteSettingsBroadcastHandler.receivedBroadcastMessage(
   1349      version,
   1350      BROADCAST_ID,
   1351      context
   1352    );
   1353    Assert.equal(passedTrigger, "broadcast");
   1354  }
   1355 );
   1356 add_task(clear_state);