tor-browser

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

test_addons_store.js (29421B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const { AddonsEngine } = ChromeUtils.importESModule(
      7  "resource://services-sync/engines/addons.sys.mjs"
      8 );
      9 const { Service } = ChromeUtils.importESModule(
     10  "resource://services-sync/service.sys.mjs"
     11 );
     12 const { SyncedRecordsTelemetry } = ChromeUtils.importESModule(
     13  "resource://services-sync/telemetry.sys.mjs"
     14 );
     15 
     16 const HTTP_PORT = 8888;
     17 
     18 Services.prefs.setStringPref(
     19  "extensions.getAddons.get.url",
     20  "http://localhost:8888/search/guid:%IDS%"
     21 );
     22 // Note that all compat-override URLs currently 404, but that's OK - the main
     23 // thing is to avoid us hitting the real AMO.
     24 Services.prefs.setStringPref(
     25  "extensions.getAddons.compatOverides.url",
     26  "http://localhost:8888/compat-override/guid:%IDS%"
     27 );
     28 Services.prefs.setBoolPref("extensions.install.requireSecureOrigin", false);
     29 Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
     30 
     31 AddonTestUtils.init(this);
     32 AddonTestUtils.createAppInfo(
     33  "xpcshell@tests.mozilla.org",
     34  "XPCShell",
     35  "1",
     36  "1.9.2"
     37 );
     38 AddonTestUtils.overrideCertDB();
     39 
     40 Services.prefs.setBoolPref("extensions.experiments.enabled", true);
     41 
     42 const SYSTEM_ADDON_ID = "system1@tests.mozilla.org";
     43 const THEME_ID = "synctheme@tests.mozilla.org";
     44 
     45 add_setup(async function setupBuiltInAddon() {
     46  // Enable SCOPE_APPLICATION for builtin testing.  Default in tests is only SCOPE_PROFILE.
     47  let scopes = AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION;
     48  Services.prefs.setIntPref("extensions.enabledScopes", scopes);
     49 
     50  const addon_version = "1.0";
     51  const addon_res_url_path = "test-builtin-addon";
     52 
     53  let xpi = await AddonTestUtils.createTempWebExtensionFile({
     54    manifest: {
     55      version: addon_version,
     56      browser_specific_settings: { gecko: { id: SYSTEM_ADDON_ID } },
     57    },
     58  });
     59 
     60  // The built-in location requires a resource: URL that maps to a
     61  // jar: or file: URL.  This would typically be something bundled
     62  // into omni.ja but for testing we just use a temp file.
     63  let base = Services.io.newURI(`jar:file:${xpi.path}!/`);
     64  let resProto = Services.io
     65    .getProtocolHandler("resource")
     66    .QueryInterface(Ci.nsIResProtocolHandler);
     67  resProto.setSubstitution(addon_res_url_path, base);
     68  let builtins = [
     69    {
     70      addon_id: SYSTEM_ADDON_ID,
     71      addon_version,
     72      res_url: `resource://${addon_res_url_path}/`,
     73    },
     74  ];
     75  await AddonTestUtils.overrideBuiltIns({ builtins });
     76  await AddonTestUtils.promiseStartupManager();
     77 });
     78 
     79 const ID1 = "addon1@tests.mozilla.org";
     80 const ID2 = "addon2@tests.mozilla.org";
     81 const ID3 = "addon3@tests.mozilla.org";
     82 
     83 const ADDONS = {
     84  test_addon1: {
     85    manifest: {
     86      browser_specific_settings: {
     87        gecko: {
     88          id: ID1,
     89          update_url: "http://example.com/data/test_install.json",
     90        },
     91      },
     92    },
     93  },
     94 
     95  test_addon2: {
     96    manifest: {
     97      browser_specific_settings: { gecko: { id: ID2 } },
     98    },
     99  },
    100 
    101  test_addon3: {
    102    manifest: {
    103      browser_specific_settings: {
    104        gecko: {
    105          id: ID3,
    106          strict_max_version: "0",
    107        },
    108      },
    109    },
    110  },
    111 };
    112 
    113 const SEARCH_RESULT = {
    114  next: null,
    115  results: [
    116    {
    117      name: "Test Extension",
    118      type: "extension",
    119      guid: "addon1@tests.mozilla.org",
    120      current_version: {
    121        version: "1.0",
    122        files: [
    123          {
    124            platform: "all",
    125            size: 485,
    126            url: "http://localhost:8888/addon1.xpi",
    127          },
    128        ],
    129      },
    130      last_updated: "2018-10-27T04:12:00.826Z",
    131    },
    132  ],
    133 };
    134 
    135 const MISSING_SEARCH_RESULT = {
    136  next: null,
    137  results: [
    138    {
    139      name: "Test",
    140      type: "extension",
    141      guid: "missing-xpi@tests.mozilla.org",
    142      current_version: {
    143        version: "1.0",
    144        files: [
    145          {
    146            platform: "all",
    147            size: 123,
    148            url: "http://localhost:8888/THIS_DOES_NOT_EXIST.xpi",
    149          },
    150        ],
    151      },
    152    },
    153  ],
    154 };
    155 
    156 const AMOSIGNED_SHA1_SEARCH_RESULT = {
    157  next: null,
    158  results: [
    159    {
    160      name: "Test Extension",
    161      type: "extension",
    162      guid: "amosigned-xpi@tests.mozilla.org",
    163      current_version: {
    164        version: "2.1",
    165        files: [
    166          {
    167            platform: "all",
    168            size: 4287,
    169            url: "http://localhost:8888/amosigned-sha1only.xpi",
    170          },
    171        ],
    172      },
    173      last_updated: "2024-03-21T16:00:06.640Z",
    174    },
    175  ],
    176 };
    177 
    178 const XPIS = {};
    179 for (let [name, files] of Object.entries(ADDONS)) {
    180  XPIS[name] = AddonTestUtils.createTempWebExtensionFile(files);
    181 }
    182 
    183 let engine;
    184 let store;
    185 let reconciler;
    186 
    187 const proxyService = Cc[
    188  "@mozilla.org/network/protocol-proxy-service;1"
    189 ].getService(Ci.nsIProtocolProxyService);
    190 
    191 const proxyFilter = {
    192  proxyInfo: proxyService.newProxyInfo(
    193    "http",
    194    "localhost",
    195    HTTP_PORT,
    196    "",
    197    "",
    198    0,
    199    4096,
    200    null
    201  ),
    202 
    203  applyFilter(channel, defaultProxyInfo, callback) {
    204    if (channel.URI.host === "example.com") {
    205      callback.onProxyFilterResult(this.proxyInfo);
    206    } else {
    207      callback.onProxyFilterResult(defaultProxyInfo);
    208    }
    209  },
    210 };
    211 
    212 proxyService.registerChannelFilter(proxyFilter, 0);
    213 registerCleanupFunction(() => {
    214  proxyService.unregisterChannelFilter(proxyFilter);
    215 });
    216 
    217 /**
    218 * Create a AddonsRec for this application with the fields specified.
    219 *
    220 * @param  id       Sync GUID of record
    221 * @param  addonId  ID of add-on
    222 * @param  enabled  Boolean whether record is enabled
    223 * @param  deleted  Boolean whether record was deleted
    224 */
    225 function createRecordForThisApp(id, addonId, enabled, deleted) {
    226  return {
    227    id,
    228    addonID: addonId,
    229    enabled,
    230    deleted: !!deleted,
    231    applicationID: Services.appinfo.ID,
    232    source: "amo",
    233  };
    234 }
    235 
    236 function createAndStartHTTPServer(port) {
    237  try {
    238    let server = new HttpServer();
    239 
    240    server.registerPathHandler(
    241      "/search/guid:addon1%40tests.mozilla.org",
    242      (req, resp) => {
    243        resp.setHeader("Content-type", "application/json", true);
    244        resp.write(JSON.stringify(SEARCH_RESULT));
    245      }
    246    );
    247    server.registerPathHandler(
    248      "/search/guid:missing-xpi%40tests.mozilla.org",
    249      (req, resp) => {
    250        resp.setHeader("Content-type", "application/json", true);
    251        resp.write(JSON.stringify(MISSING_SEARCH_RESULT));
    252      }
    253    );
    254    server.registerFile("/addon1.xpi", XPIS.test_addon1);
    255 
    256    server.registerPathHandler(
    257      "/search/guid:amosigned-xpi%40tests.mozilla.org",
    258      (req, resp) => {
    259        resp.setHeader("Content-type", "application/json", true);
    260        resp.write(JSON.stringify(AMOSIGNED_SHA1_SEARCH_RESULT));
    261      }
    262    );
    263    server.registerFile(
    264      "/amosigned-sha1only.xpi",
    265      do_get_file("amosigned-sha1only.xpi")
    266    );
    267 
    268    server.start(port);
    269 
    270    return server;
    271  } catch (ex) {
    272    _("Got exception starting HTTP server on port " + port);
    273    _("Error: " + Log.exceptionStr(ex));
    274    do_throw(ex);
    275  }
    276  return null; /* not hit, but keeps eslint happy! */
    277 }
    278 
    279 // A helper function to ensure that the reconciler's current view of the addon
    280 // is the same as the addon itself. If it's not, then the reconciler missed a
    281 // change, and is likely to re-upload the addon next sync because of the change
    282 // it missed.
    283 async function checkReconcilerUpToDate(addon) {
    284  let stateBefore = Object.assign({}, store.reconciler.addons[addon.id]);
    285  await store.reconciler.rectifyStateFromAddon(addon);
    286  let stateAfter = store.reconciler.addons[addon.id];
    287  deepEqual(stateBefore, stateAfter);
    288 }
    289 
    290 add_setup(async function setup() {
    291  await Service.engineManager.register(AddonsEngine);
    292  engine = Service.engineManager.get("addons");
    293  store = engine._store;
    294  reconciler = engine._reconciler;
    295 
    296  reconciler.startListening();
    297 
    298  // Don't flush to disk in the middle of an event listener!
    299  // This causes test hangs on WinXP.
    300  reconciler._shouldPersist = false;
    301 });
    302 
    303 add_task(async function test_remove() {
    304  _("Ensure removing add-ons from deleted records works.");
    305 
    306  let addon = await installAddon(XPIS.test_addon1, reconciler);
    307  let record = createRecordForThisApp(addon.syncGUID, ID1, true, true);
    308  let countTelemetry = new SyncedRecordsTelemetry();
    309  let failed = await store.applyIncomingBatch([record], countTelemetry);
    310  Assert.equal(0, failed.length);
    311  Assert.equal(null, countTelemetry.failedReasons);
    312  Assert.equal(0, countTelemetry.incomingCounts.failed);
    313 
    314  let newAddon = await AddonManager.getAddonByID(ID1);
    315  Assert.equal(null, newAddon);
    316 });
    317 
    318 add_task(async function test_apply_enabled() {
    319  let countTelemetry = new SyncedRecordsTelemetry();
    320  _("Ensures that changes to the userEnabled flag apply.");
    321 
    322  let addon = await installAddon(XPIS.test_addon1, reconciler);
    323  Assert.ok(addon.isActive);
    324  Assert.ok(!addon.userDisabled);
    325 
    326  _("Ensure application of a disable record works as expected.");
    327  let records = [];
    328  records.push(createRecordForThisApp(addon.syncGUID, ID1, false, false));
    329 
    330  let [failed] = await Promise.all([
    331    store.applyIncomingBatch(records, countTelemetry),
    332    AddonTestUtils.promiseAddonEvent("onDisabled"),
    333  ]);
    334  Assert.equal(0, failed.length);
    335  Assert.equal(0, countTelemetry.incomingCounts.failed);
    336  addon = await AddonManager.getAddonByID(ID1);
    337  Assert.ok(addon.userDisabled);
    338  await checkReconcilerUpToDate(addon);
    339  records = [];
    340 
    341  _("Ensure enable record works as expected.");
    342  records.push(createRecordForThisApp(addon.syncGUID, ID1, true, false));
    343  [failed] = await Promise.all([
    344    store.applyIncomingBatch(records, countTelemetry),
    345    AddonTestUtils.promiseWebExtensionStartup(ID1),
    346  ]);
    347  Assert.equal(0, failed.length);
    348  Assert.equal(0, countTelemetry.incomingCounts.failed);
    349  addon = await AddonManager.getAddonByID(ID1);
    350  Assert.ok(!addon.userDisabled);
    351  await checkReconcilerUpToDate(addon);
    352  records = [];
    353 
    354  _("Ensure enabled state updates don't apply if the ignore pref is set.");
    355  records.push(createRecordForThisApp(addon.syncGUID, ID1, false, false));
    356  Svc.PrefBranch.setBoolPref("addons.ignoreUserEnabledChanges", true);
    357  failed = await store.applyIncomingBatch(records, countTelemetry);
    358  Assert.equal(0, failed.length);
    359  Assert.equal(0, countTelemetry.incomingCounts.failed);
    360  addon = await AddonManager.getAddonByID(ID1);
    361  Assert.ok(!addon.userDisabled);
    362  records = [];
    363 
    364  await uninstallAddon(addon, reconciler);
    365  Svc.PrefBranch.clearUserPref("addons.ignoreUserEnabledChanges");
    366 });
    367 
    368 add_task(async function test_apply_enabled_appDisabled() {
    369  _(
    370    "Ensures that changes to the userEnabled flag apply when the addon is appDisabled."
    371  );
    372 
    373  // this addon is appDisabled by default.
    374  let addon = await installAddon(XPIS.test_addon3);
    375  Assert.ok(addon.appDisabled);
    376  Assert.ok(!addon.isActive);
    377  Assert.ok(!addon.userDisabled);
    378 
    379  _("Ensure application of a disable record works as expected.");
    380  store.reconciler.pruneChangesBeforeDate(Date.now() + 10);
    381  store.reconciler._changes = [];
    382  let records = [];
    383  let countTelemetry = new SyncedRecordsTelemetry();
    384  records.push(createRecordForThisApp(addon.syncGUID, ID3, false, false));
    385  let failed = await store.applyIncomingBatch(records, countTelemetry);
    386  Assert.equal(0, failed.length);
    387  Assert.equal(0, countTelemetry.incomingCounts.failed);
    388  addon = await AddonManager.getAddonByID(ID3);
    389  Assert.ok(addon.userDisabled);
    390  await checkReconcilerUpToDate(addon);
    391  records = [];
    392 
    393  _("Ensure enable record works as expected.");
    394  records.push(createRecordForThisApp(addon.syncGUID, ID3, true, false));
    395  failed = await store.applyIncomingBatch(records, countTelemetry);
    396  Assert.equal(0, failed.length);
    397  Assert.equal(0, countTelemetry.incomingCounts.failed);
    398  addon = await AddonManager.getAddonByID(ID3);
    399  Assert.ok(!addon.userDisabled);
    400  await checkReconcilerUpToDate(addon);
    401  records = [];
    402 
    403  await uninstallAddon(addon, reconciler);
    404 });
    405 
    406 add_task(async function test_ignore_different_appid() {
    407  _(
    408    "Ensure that incoming records with a different application ID are ignored."
    409  );
    410 
    411  // We test by creating a record that should result in an update.
    412  let addon = await installAddon(XPIS.test_addon1, reconciler);
    413  Assert.ok(!addon.userDisabled);
    414 
    415  let record = createRecordForThisApp(addon.syncGUID, ID1, false, false);
    416  record.applicationID = "FAKE_ID";
    417  let countTelemetry = new SyncedRecordsTelemetry();
    418  let failed = await store.applyIncomingBatch([record], countTelemetry);
    419  Assert.equal(0, failed.length);
    420 
    421  let newAddon = await AddonManager.getAddonByID(ID1);
    422  Assert.ok(!newAddon.userDisabled);
    423 
    424  await uninstallAddon(addon, reconciler);
    425 });
    426 
    427 add_task(async function test_ignore_unknown_source() {
    428  _("Ensure incoming records with unknown source are ignored.");
    429 
    430  let addon = await installAddon(XPIS.test_addon1, reconciler);
    431 
    432  let record = createRecordForThisApp(addon.syncGUID, ID1, false, false);
    433  record.source = "DUMMY_SOURCE";
    434  let countTelemetry = new SyncedRecordsTelemetry();
    435  let failed = await store.applyIncomingBatch([record], countTelemetry);
    436  Assert.equal(0, failed.length);
    437 
    438  let newAddon = await AddonManager.getAddonByID(ID1);
    439  Assert.ok(!newAddon.userDisabled);
    440 
    441  await uninstallAddon(addon, reconciler);
    442 });
    443 
    444 add_task(async function test_apply_uninstall() {
    445  _("Ensures that uninstalling an add-on from a record works.");
    446 
    447  let addon = await installAddon(XPIS.test_addon1, reconciler);
    448 
    449  let records = [];
    450  let countTelemetry = new SyncedRecordsTelemetry();
    451  records.push(createRecordForThisApp(addon.syncGUID, ID1, true, true));
    452  let failed = await store.applyIncomingBatch(records, countTelemetry);
    453  Assert.equal(0, failed.length);
    454  Assert.equal(0, countTelemetry.incomingCounts.failed);
    455 
    456  addon = await AddonManager.getAddonByID(ID1);
    457  Assert.equal(null, addon);
    458 });
    459 
    460 add_task(async function test_addon_syncability() {
    461  _("Ensure isAddonSyncable functions properly.");
    462 
    463  Svc.PrefBranch.setStringPref(
    464    "addons.trustedSourceHostnames",
    465    "addons.mozilla.org,other.example.com"
    466  );
    467 
    468  Assert.ok(!(await store.isAddonSyncable(null)));
    469 
    470  let addon = await installAddon(XPIS.test_addon1, reconciler);
    471  Assert.ok(await store.isAddonSyncable(addon));
    472 
    473  let dummy = {};
    474  const KEYS = [
    475    "id",
    476    "syncGUID",
    477    "type",
    478    "scope",
    479    "foreignInstall",
    480    "isSyncable",
    481  ];
    482  for (let k of KEYS) {
    483    dummy[k] = addon[k];
    484  }
    485 
    486  Assert.ok(await store.isAddonSyncable(dummy));
    487 
    488  dummy.type = "UNSUPPORTED";
    489  Assert.ok(!(await store.isAddonSyncable(dummy)));
    490  dummy.type = addon.type;
    491 
    492  dummy.scope = 0;
    493  Assert.ok(!(await store.isAddonSyncable(dummy)));
    494  dummy.scope = addon.scope;
    495 
    496  dummy.isSyncable = false;
    497  Assert.ok(!(await store.isAddonSyncable(dummy)));
    498  dummy.isSyncable = addon.isSyncable;
    499 
    500  dummy.foreignInstall = true;
    501  Assert.ok(!(await store.isAddonSyncable(dummy)));
    502  dummy.foreignInstall = false;
    503 
    504  await uninstallAddon(addon, reconciler);
    505 
    506  Assert.ok(!store.isSourceURITrusted(null));
    507 
    508  let trusted = [
    509    "https://addons.mozilla.org/foo",
    510    "https://other.example.com/foo",
    511  ];
    512 
    513  let untrusted = [
    514    "http://addons.mozilla.org/foo", // non-https
    515    "ftps://addons.mozilla.org/foo", // non-https
    516    "https://untrusted.example.com/foo", // non-trusted hostname`
    517  ];
    518 
    519  for (let uri of trusted) {
    520    Assert.ok(store.isSourceURITrusted(Services.io.newURI(uri)));
    521  }
    522 
    523  for (let uri of untrusted) {
    524    Assert.ok(!store.isSourceURITrusted(Services.io.newURI(uri)));
    525  }
    526 
    527  Svc.PrefBranch.setStringPref("addons.trustedSourceHostnames", "");
    528  for (let uri of trusted) {
    529    Assert.ok(!store.isSourceURITrusted(Services.io.newURI(uri)));
    530  }
    531 
    532  Svc.PrefBranch.setStringPref(
    533    "addons.trustedSourceHostnames",
    534    "addons.mozilla.org"
    535  );
    536  Assert.ok(
    537    store.isSourceURITrusted(
    538      Services.io.newURI("https://addons.mozilla.org/foo")
    539    )
    540  );
    541 
    542  Svc.PrefBranch.clearUserPref("addons.trustedSourceHostnames");
    543 });
    544 
    545 add_task(async function test_get_all_ids() {
    546  _("Ensures that getAllIDs() returns an appropriate set.");
    547 
    548  _("Installing two addons.");
    549  // XXX - this test seems broken - at this point, before we've installed the
    550  // addons below, store.getAllIDs() returns all addons installed by previous
    551  // tests, even though those tests uninstalled the addon.
    552  // So if any tests above ever add a new addon ID, they are going to need to
    553  // be added here too.
    554  // Assert.equal(0, Object.keys(store.getAllIDs()).length);
    555  let addon1 = await installAddon(XPIS.test_addon1, reconciler);
    556  let addon2 = await installAddon(XPIS.test_addon2, reconciler);
    557  let addon3 = await installAddon(XPIS.test_addon3, reconciler);
    558 
    559  _("Ensure they're syncable.");
    560  Assert.ok(await store.isAddonSyncable(addon1));
    561  Assert.ok(await store.isAddonSyncable(addon2));
    562  Assert.ok(await store.isAddonSyncable(addon3));
    563 
    564  let ids = await store.getAllIDs();
    565 
    566  Assert.equal("object", typeof ids);
    567  Assert.equal(3, Object.keys(ids).length);
    568  Assert.ok(addon1.syncGUID in ids);
    569  Assert.ok(addon2.syncGUID in ids);
    570  Assert.ok(addon3.syncGUID in ids);
    571 
    572  await uninstallAddon(addon1, reconciler);
    573  await uninstallAddon(addon2, reconciler);
    574  await uninstallAddon(addon3, reconciler);
    575 });
    576 
    577 add_task(async function test_change_item_id() {
    578  _("Ensures that changeItemID() works properly.");
    579 
    580  let addon = await installAddon(XPIS.test_addon1, reconciler);
    581 
    582  let oldID = addon.syncGUID;
    583  let newID = Utils.makeGUID();
    584 
    585  await store.changeItemID(oldID, newID);
    586 
    587  let newAddon = await AddonManager.getAddonByID(ID1);
    588  Assert.notEqual(null, newAddon);
    589  Assert.equal(newID, newAddon.syncGUID);
    590 
    591  await uninstallAddon(newAddon, reconciler);
    592 });
    593 
    594 add_task(async function test_create() {
    595  _("Ensure creating/installing an add-on from a record works.");
    596 
    597  let server = createAndStartHTTPServer(HTTP_PORT);
    598 
    599  let guid = Utils.makeGUID();
    600  let record = createRecordForThisApp(guid, ID1, true, false);
    601  let countTelemetry = new SyncedRecordsTelemetry();
    602  let failed = await store.applyIncomingBatch([record], countTelemetry);
    603  Assert.equal(0, failed.length);
    604 
    605  let newAddon = await AddonManager.getAddonByID(ID1);
    606  Assert.notEqual(null, newAddon);
    607  Assert.equal(guid, newAddon.syncGUID);
    608  Assert.ok(!newAddon.userDisabled);
    609 
    610  await uninstallAddon(newAddon, reconciler);
    611 
    612  await promiseStopServer(server);
    613 });
    614 
    615 add_task(async function test_weak_signature_restrictions() {
    616  _("Ensure installing add-ons with a weak signature fails when restricted.");
    617 
    618  // Ensure restrictions on weak signatures are enabled (this should be removed when
    619  // the new behavior is riding the train).
    620  const resetWeakSignaturePref =
    621    AddonTestUtils.setWeakSignatureInstallAllowed(false);
    622  const server = createAndStartHTTPServer(HTTP_PORT);
    623  const ID_TEST_SHA1 = "amosigned-xpi@tests.mozilla.org";
    624 
    625  const guidKO = Utils.makeGUID();
    626  const guidOK = Utils.makeGUID();
    627  const recordKO = createRecordForThisApp(guidKO, ID_TEST_SHA1, true, false);
    628  const recordOK = createRecordForThisApp(guidOK, ID1, true, false);
    629  const countTelemetry = new SyncedRecordsTelemetry();
    630 
    631  let failed;
    632 
    633  const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
    634    failed = await store.applyIncomingBatch(
    635      [recordKO, recordOK],
    636      countTelemetry
    637    );
    638  });
    639 
    640  Assert.equal(
    641    1,
    642    failed.length,
    643    "Expect only 1 on the two synced add-ons to fail"
    644  );
    645 
    646  resetWeakSignaturePref();
    647 
    648  let addonKO = await AddonManager.getAddonByID(ID_TEST_SHA1);
    649  Assert.equal(null, addonKO, `Expect ${ID_TEST_SHA1} to NOT be installed`);
    650  let addonOK = await AddonManager.getAddonByID(ID1);
    651  Assert.notEqual(null, addonOK, `Expect ${ID1} to be installed`);
    652 
    653  await uninstallAddon(addonOK, reconciler);
    654  await promiseStopServer(server);
    655 
    656  AddonTestUtils.checkMessages(messages, {
    657    expected: [
    658      {
    659        message:
    660          /Download of .*\/amosigned-sha1only.xpi failed: install rejected due to the package not including a strong cryptographic signature/,
    661      },
    662    ],
    663  });
    664 });
    665 
    666 add_task(async function test_create_missing_search() {
    667  _("Ensures that failed add-on searches are handled gracefully.");
    668 
    669  let server = createAndStartHTTPServer(HTTP_PORT);
    670 
    671  // The handler for this ID is not installed, so a search should 404.
    672  const id = "missing@tests.mozilla.org";
    673  let guid = Utils.makeGUID();
    674  let record = createRecordForThisApp(guid, id, true, false);
    675  let countTelemetry = new SyncedRecordsTelemetry();
    676  let failed = await store.applyIncomingBatch([record], countTelemetry);
    677  Assert.equal(1, failed.length);
    678  Assert.equal(guid, failed[0]);
    679  Assert.equal(
    680    countTelemetry.incomingCounts.failedReasons[0].name,
    681    "GET <URL> failed (status 404)"
    682  );
    683  Assert.equal(countTelemetry.incomingCounts.failedReasons[0].count, 1);
    684 
    685  let addon = await AddonManager.getAddonByID(id);
    686  Assert.equal(null, addon);
    687 
    688  await promiseStopServer(server);
    689 });
    690 
    691 add_task(async function test_create_bad_install() {
    692  _("Ensures that add-ons without a valid install are handled gracefully.");
    693 
    694  let server = createAndStartHTTPServer(HTTP_PORT);
    695 
    696  // The handler returns a search result but the XPI will 404.
    697  const id = "missing-xpi@tests.mozilla.org";
    698  let guid = Utils.makeGUID();
    699  let record = createRecordForThisApp(guid, id, true, false);
    700  let countTelemetry = new SyncedRecordsTelemetry();
    701  /* let failed = */ await store.applyIncomingBatch([record], countTelemetry);
    702  // This addon had no source URI so was skipped - but it's not treated as
    703  // failure.
    704  // XXX - this test isn't testing what we thought it was. Previously the addon
    705  // was not being installed due to requireSecureURL checking *before* we'd
    706  // attempted to get the XPI.
    707  // With requireSecureURL disabled we do see a download failure, but the addon
    708  // *does* get added to |failed|.
    709  // FTR: onDownloadFailed() is called with ERROR_NETWORK_FAILURE, so it's going
    710  // to be tricky to distinguish a 404 from other transient network errors
    711  // where we do want the addon to end up in |failed|.
    712  // This is being tracked in bug 1284778.
    713  // Assert.equal(0, failed.length);
    714 
    715  let addon = await AddonManager.getAddonByID(id);
    716  Assert.equal(null, addon);
    717 
    718  await promiseStopServer(server);
    719 });
    720 
    721 add_task(async function test_ignore_system() {
    722  _("Ensure we ignore system addons");
    723  // Our system addon should not appear in getAllIDs
    724  await engine._refreshReconcilerState();
    725  let num = 0;
    726  let ids = await store.getAllIDs();
    727  for (let guid in ids) {
    728    num += 1;
    729    let addon = reconciler.getAddonStateFromSyncGUID(guid);
    730    Assert.notEqual(addon.id, SYSTEM_ADDON_ID);
    731  }
    732  Assert.greater(num, 1, "should have seen at least one.");
    733 });
    734 
    735 add_task(async function test_incoming_system() {
    736  _("Ensure we handle incoming records that refer to a system addon");
    737  // eg, loop initially had a normal addon but it was then "promoted" to be a
    738  // system addon but wanted to keep the same ID. The server record exists due
    739  // to this.
    740 
    741  // before we start, ensure the system addon isn't disabled.
    742  Assert.ok(!(await AddonManager.getAddonByID(SYSTEM_ADDON_ID).userDisabled));
    743 
    744  // Now simulate an incoming record with the same ID as the system addon,
    745  // but flagged as disabled - it should not be applied.
    746  let server = createAndStartHTTPServer(HTTP_PORT);
    747  // We make the incoming record flag the system addon as disabled - it should
    748  // be ignored.
    749  let guid = Utils.makeGUID();
    750  let record = createRecordForThisApp(guid, SYSTEM_ADDON_ID, false, false);
    751  let countTelemetry = new SyncedRecordsTelemetry();
    752  let failed = await store.applyIncomingBatch([record], countTelemetry);
    753  Assert.equal(0, failed.length);
    754 
    755  // The system addon should still not be userDisabled.
    756  Assert.ok(!(await AddonManager.getAddonByID(SYSTEM_ADDON_ID).userDisabled));
    757 
    758  await promiseStopServer(server);
    759 });
    760 
    761 add_task(async function test_wipe() {
    762  _("Ensures that wiping causes add-ons to be uninstalled.");
    763 
    764  await installAddon(XPIS.test_addon1, reconciler);
    765 
    766  await store.wipe();
    767 
    768  let addon = await AddonManager.getAddonByID(ID1);
    769  Assert.equal(null, addon);
    770 });
    771 
    772 add_task(async function test_wipe_and_install() {
    773  _("Ensure wipe followed by install works.");
    774 
    775  // This tests the reset sync flow where remote data is replaced by local. The
    776  // receiving client will see a wipe followed by a record which should undo
    777  // the wipe.
    778  let installed = await installAddon(XPIS.test_addon1, reconciler);
    779 
    780  let record = createRecordForThisApp(installed.syncGUID, ID1, true, false);
    781 
    782  await store.wipe();
    783 
    784  let deleted = await AddonManager.getAddonByID(ID1);
    785  Assert.equal(null, deleted);
    786 
    787  // Re-applying the record can require re-fetching the XPI.
    788  let server = createAndStartHTTPServer(HTTP_PORT);
    789 
    790  await store.applyIncoming(record);
    791 
    792  let fetched = await AddonManager.getAddonByID(record.addonID);
    793  Assert.ok(!!fetched);
    794 
    795  // wipe again to we are left with a clean slate.
    796  await store.wipe();
    797 
    798  await promiseStopServer(server);
    799 });
    800 
    801 // STR for what this is testing:
    802 // * Either:
    803 //   * Install then remove an addon, then delete addons.json from the profile
    804 //     or corrupt it (in which case the addon manager will remove it)
    805 //   * Install then remove an addon while addon caching is disabled, then
    806 //     re-enable addon caching.
    807 // * Install the same addon in a different profile, sync it.
    808 // * Sync this profile
    809 // Before bug 1467904, the addon would fail to install because this profile
    810 // has a copy of the addon in our addonsreconciler.json, but the addon manager
    811 // does *not* have a copy in its cache, and repopulating that cache would not
    812 // re-add it as the addon is no longer installed locally.
    813 add_task(async function test_incoming_reconciled_but_not_cached() {
    814  _(
    815    "Ensure we handle incoming records our reconciler has but the addon cache does not"
    816  );
    817 
    818  // Make sure addon is not installed.
    819  let addon = await AddonManager.getAddonByID(ID1);
    820  Assert.equal(null, addon);
    821 
    822  Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", false);
    823 
    824  addon = await installAddon(XPIS.test_addon1, reconciler);
    825  Assert.notEqual(await AddonManager.getAddonByID(ID1), null);
    826  await uninstallAddon(addon, reconciler);
    827 
    828  Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", true);
    829 
    830  // now pretend it is incoming.
    831  let server = createAndStartHTTPServer(HTTP_PORT);
    832  let guid = Utils.makeGUID();
    833  let record = createRecordForThisApp(guid, ID1, true, false);
    834  let countTelemetry = new SyncedRecordsTelemetry();
    835  let failed = await store.applyIncomingBatch([record], countTelemetry);
    836  Assert.equal(0, failed.length);
    837 
    838  Assert.notEqual(await AddonManager.getAddonByID(ID1), null);
    839 
    840  await promiseStopServer(server);
    841 });
    842 
    843 // Helper for testing theme-specific addons
    844 function makeThemeSearchResult(id) {
    845  return {
    846    next: null,
    847    results: [
    848      {
    849        name: "Sync Theme",
    850        type: "theme",
    851        guid: id,
    852        current_version: {
    853          version: "1.0",
    854          files: [
    855            {
    856              platform: "all",
    857              size: 1234,
    858              url: `http://localhost:${HTTP_PORT}/synctheme.xpi`,
    859            },
    860          ],
    861        },
    862        last_updated: "2025-03-01T00:00:00.000Z",
    863      },
    864    ],
    865  };
    866 }
    867 
    868 /**
    869 * Incoming theme add-on record should
    870 *   – install the theme
    871 *   – enable it immediately
    872 *   – clear the hand-off pref
    873 */
    874 add_task(async function test_incoming_theme_gets_enabled() {
    875  const xpiTheme = AddonTestUtils.createTempWebExtensionFile({
    876    manifest: {
    877      manifest_version: 2,
    878      name: "Sync Theme",
    879      version: "1.0",
    880      applications: { gecko: { id: THEME_ID } },
    881      theme: { colors: { frame: "#000000", tab_background_text: "#ffffff" } },
    882    },
    883  });
    884 
    885  const server = createAndStartHTTPServer(HTTP_PORT);
    886  server.registerFile("/synctheme.xpi", xpiTheme);
    887  server.registerPathHandler(
    888    `/search/guid:${encodeURIComponent(THEME_ID)}`,
    889    (req, resp) => {
    890      resp.setHeader("Content-Type", "application/json", true);
    891      resp.write(JSON.stringify(makeThemeSearchResult(THEME_ID)));
    892    }
    893  );
    894 
    895  // Pretend Prefs‑engine has just synced a new activeThemeID
    896  Services.prefs.setStringPref("extensions.pendingActiveThemeID", THEME_ID);
    897 
    898  // Feed an add-on record into the Addons engine.
    899  const guid = Utils.makeGUID();
    900  const record = createRecordForThisApp(guid, THEME_ID, true, false);
    901  const telem = new SyncedRecordsTelemetry();
    902 
    903  const onStartup = AddonTestUtils.promiseWebExtensionStartup(THEME_ID);
    904  const failed = await store.applyIncomingBatch([record], telem);
    905 
    906  Assert.equal(0, failed.length, "No records should fail to apply");
    907  await onStartup;
    908 
    909  const theme = await AddonManager.getAddonByID(THEME_ID);
    910  Assert.ok(theme, "Theme is installed");
    911  Assert.ok(theme.isActive, "Theme is active");
    912  Assert.ok(!theme.userDisabled, "Theme is not user-disabled");
    913 
    914  Assert.equal(
    915    Services.prefs.getPrefType("extensions.pendingActiveThemeID"),
    916    Ci.nsIPrefBranch.PREF_INVALID,
    917    "Hand-off pref was cleared"
    918  );
    919 
    920  // Clean-up
    921  await uninstallAddon(theme, reconciler);
    922  await promiseStopServer(server);
    923 });
    924 
    925 // NOTE: The test above must be the last test run due to the addon cache
    926 // being trashed. It is probably possible to fix that by running, eg,
    927 // AddonRespository.backgroundUpdateCheck() to rebuild the cache, but that
    928 // requires implementing more AMO functionality in our test server
    929 
    930 add_task(async function cleanup() {
    931  // There's an xpcom-shutdown hook for this, but let's give this a shot.
    932  reconciler.stopListening();
    933 });