diff options
Diffstat (limited to 'services/sync/tests/unit/test_addons_store.js')
-rw-r--r-- | services/sync/tests/unit/test_addons_store.js | 753 |
1 files changed, 753 insertions, 0 deletions
diff --git a/services/sync/tests/unit/test_addons_store.js b/services/sync/tests/unit/test_addons_store.js new file mode 100644 index 0000000000..444f0e912c --- /dev/null +++ b/services/sync/tests/unit/test_addons_store.js @@ -0,0 +1,753 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Preferences } = ChromeUtils.importESModule( + "resource://gre/modules/Preferences.sys.mjs" +); +const { AddonsEngine } = ChromeUtils.importESModule( + "resource://services-sync/engines/addons.sys.mjs" +); +const { Service } = ChromeUtils.importESModule( + "resource://services-sync/service.sys.mjs" +); +const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +const { SyncedRecordsTelemetry } = ChromeUtils.importESModule( + "resource://services-sync/telemetry.sys.mjs" +); + +const HTTP_PORT = 8888; + +const prefs = new Preferences(); + +prefs.set( + "extensions.getAddons.get.url", + "http://localhost:8888/search/guid:%IDS%" +); +// Note that all compat-override URLs currently 404, but that's OK - the main +// thing is to avoid us hitting the real AMO. +prefs.set( + "extensions.getAddons.compatOverides.url", + "http://localhost:8888/compat-override/guid:%IDS%" +); +prefs.set("extensions.install.requireSecureOrigin", false); +prefs.set("extensions.checkUpdateSecurity", false); + +AddonTestUtils.init(this); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "1.9.2" +); +AddonTestUtils.overrideCertDB(); + +Services.prefs.setCharPref("extensions.minCompatibleAppVersion", "0"); +Services.prefs.setCharPref("extensions.minCompatiblePlatformVersion", "0"); +Services.prefs.setBoolPref("extensions.experiments.enabled", true); + +const SYSTEM_ADDON_ID = "system1@tests.mozilla.org"; +add_task(async function setupSystemAddon() { + const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"], true); + AddonTestUtils.registerDirectory("XREAppFeat", distroDir); + + let xpi = await AddonTestUtils.createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { gecko: { id: SYSTEM_ADDON_ID } }, + }, + }); + + xpi.copyTo(distroDir, `${SYSTEM_ADDON_ID}.xpi`); + + await AddonTestUtils.overrideBuiltIns({ system: [SYSTEM_ADDON_ID] }); + await AddonTestUtils.promiseStartupManager(); +}); + +const ID1 = "addon1@tests.mozilla.org"; +const ID2 = "addon2@tests.mozilla.org"; +const ID3 = "addon3@tests.mozilla.org"; + +const ADDONS = { + test_addon1: { + manifest: { + browser_specific_settings: { + gecko: { + id: ID1, + update_url: "http://example.com/data/test_install.json", + }, + }, + }, + }, + + test_addon2: { + manifest: { + browser_specific_settings: { gecko: { id: ID2 } }, + }, + }, + + test_addon3: { + manifest: { + browser_specific_settings: { + gecko: { + id: ID3, + strict_max_version: "0", + }, + }, + }, + }, +}; + +const SEARCH_RESULT = { + next: null, + results: [ + { + name: "Test Extension", + type: "extension", + guid: "addon1@tests.mozilla.org", + current_version: { + version: "1.0", + files: [ + { + platform: "all", + size: 485, + url: "http://localhost:8888/addon1.xpi", + }, + ], + }, + last_updated: "2018-10-27T04:12:00.826Z", + }, + ], +}; + +const MISSING_SEARCH_RESULT = { + next: null, + results: [ + { + name: "Test", + type: "extension", + guid: "missing-xpi@tests.mozilla.org", + current_version: { + version: "1.0", + files: [ + { + platform: "all", + size: 123, + url: "http://localhost:8888/THIS_DOES_NOT_EXIST.xpi", + }, + ], + }, + }, + ], +}; + +const XPIS = {}; +for (let [name, files] of Object.entries(ADDONS)) { + XPIS[name] = AddonTestUtils.createTempWebExtensionFile(files); +} + +let engine; +let store; +let reconciler; + +const proxyService = Cc[ + "@mozilla.org/network/protocol-proxy-service;1" +].getService(Ci.nsIProtocolProxyService); + +const proxyFilter = { + proxyInfo: proxyService.newProxyInfo( + "http", + "localhost", + HTTP_PORT, + "", + "", + 0, + 4096, + null + ), + + applyFilter(channel, defaultProxyInfo, callback) { + if (channel.URI.host === "example.com") { + callback.onProxyFilterResult(this.proxyInfo); + } else { + callback.onProxyFilterResult(defaultProxyInfo); + } + }, +}; + +proxyService.registerChannelFilter(proxyFilter, 0); +registerCleanupFunction(() => { + proxyService.unregisterChannelFilter(proxyFilter); +}); + +/** + * Create a AddonsRec for this application with the fields specified. + * + * @param id Sync GUID of record + * @param addonId ID of add-on + * @param enabled Boolean whether record is enabled + * @param deleted Boolean whether record was deleted + */ +function createRecordForThisApp(id, addonId, enabled, deleted) { + return { + id, + addonID: addonId, + enabled, + deleted: !!deleted, + applicationID: Services.appinfo.ID, + source: "amo", + }; +} + +function createAndStartHTTPServer(port) { + try { + let server = new HttpServer(); + + server.registerPathHandler( + "/search/guid:addon1%40tests.mozilla.org", + (req, resp) => { + resp.setHeader("Content-type", "application/json", true); + resp.write(JSON.stringify(SEARCH_RESULT)); + } + ); + server.registerPathHandler( + "/search/guid:missing-xpi%40tests.mozilla.org", + (req, resp) => { + resp.setHeader("Content-type", "application/json", true); + resp.write(JSON.stringify(MISSING_SEARCH_RESULT)); + } + ); + server.registerFile("/addon1.xpi", XPIS.test_addon1); + + server.start(port); + + return server; + } catch (ex) { + _("Got exception starting HTTP server on port " + port); + _("Error: " + Log.exceptionStr(ex)); + do_throw(ex); + } + return null; /* not hit, but keeps eslint happy! */ +} + +// A helper function to ensure that the reconciler's current view of the addon +// is the same as the addon itself. If it's not, then the reconciler missed a +// change, and is likely to re-upload the addon next sync because of the change +// it missed. +async function checkReconcilerUpToDate(addon) { + let stateBefore = Object.assign({}, store.reconciler.addons[addon.id]); + await store.reconciler.rectifyStateFromAddon(addon); + let stateAfter = store.reconciler.addons[addon.id]; + deepEqual(stateBefore, stateAfter); +} + +add_task(async function setup() { + await Service.engineManager.register(AddonsEngine); + engine = Service.engineManager.get("addons"); + store = engine._store; + reconciler = engine._reconciler; + + reconciler.startListening(); + + // Don't flush to disk in the middle of an event listener! + // This causes test hangs on WinXP. + reconciler._shouldPersist = false; +}); + +add_task(async function test_remove() { + _("Ensure removing add-ons from deleted records works."); + + let addon = await installAddon(XPIS.test_addon1, reconciler); + let record = createRecordForThisApp(addon.syncGUID, ID1, true, true); + let countTelemetry = new SyncedRecordsTelemetry(); + let failed = await store.applyIncomingBatch([record], countTelemetry); + Assert.equal(0, failed.length); + Assert.equal(null, countTelemetry.failedReasons); + Assert.equal(0, countTelemetry.incomingCounts.failed); + + let newAddon = await AddonManager.getAddonByID(ID1); + Assert.equal(null, newAddon); +}); + +add_task(async function test_apply_enabled() { + let countTelemetry = new SyncedRecordsTelemetry(); + _("Ensures that changes to the userEnabled flag apply."); + + let addon = await installAddon(XPIS.test_addon1, reconciler); + Assert.ok(addon.isActive); + Assert.ok(!addon.userDisabled); + + _("Ensure application of a disable record works as expected."); + let records = []; + records.push(createRecordForThisApp(addon.syncGUID, ID1, false, false)); + + let [failed] = await Promise.all([ + store.applyIncomingBatch(records, countTelemetry), + AddonTestUtils.promiseAddonEvent("onDisabled"), + ]); + Assert.equal(0, failed.length); + Assert.equal(0, countTelemetry.incomingCounts.failed); + addon = await AddonManager.getAddonByID(ID1); + Assert.ok(addon.userDisabled); + await checkReconcilerUpToDate(addon); + records = []; + + _("Ensure enable record works as expected."); + records.push(createRecordForThisApp(addon.syncGUID, ID1, true, false)); + [failed] = await Promise.all([ + store.applyIncomingBatch(records, countTelemetry), + AddonTestUtils.promiseWebExtensionStartup(ID1), + ]); + Assert.equal(0, failed.length); + Assert.equal(0, countTelemetry.incomingCounts.failed); + addon = await AddonManager.getAddonByID(ID1); + Assert.ok(!addon.userDisabled); + await checkReconcilerUpToDate(addon); + records = []; + + _("Ensure enabled state updates don't apply if the ignore pref is set."); + records.push(createRecordForThisApp(addon.syncGUID, ID1, false, false)); + Svc.Prefs.set("addons.ignoreUserEnabledChanges", true); + failed = await store.applyIncomingBatch(records, countTelemetry); + Assert.equal(0, failed.length); + Assert.equal(0, countTelemetry.incomingCounts.failed); + addon = await AddonManager.getAddonByID(ID1); + Assert.ok(!addon.userDisabled); + records = []; + + await uninstallAddon(addon, reconciler); + Svc.Prefs.reset("addons.ignoreUserEnabledChanges"); +}); + +add_task(async function test_apply_enabled_appDisabled() { + _( + "Ensures that changes to the userEnabled flag apply when the addon is appDisabled." + ); + + // this addon is appDisabled by default. + let addon = await installAddon(XPIS.test_addon3); + Assert.ok(addon.appDisabled); + Assert.ok(!addon.isActive); + Assert.ok(!addon.userDisabled); + + _("Ensure application of a disable record works as expected."); + store.reconciler.pruneChangesBeforeDate(Date.now() + 10); + store.reconciler._changes = []; + let records = []; + let countTelemetry = new SyncedRecordsTelemetry(); + records.push(createRecordForThisApp(addon.syncGUID, ID3, false, false)); + let failed = await store.applyIncomingBatch(records, countTelemetry); + Assert.equal(0, failed.length); + Assert.equal(0, countTelemetry.incomingCounts.failed); + addon = await AddonManager.getAddonByID(ID3); + Assert.ok(addon.userDisabled); + await checkReconcilerUpToDate(addon); + records = []; + + _("Ensure enable record works as expected."); + records.push(createRecordForThisApp(addon.syncGUID, ID3, true, false)); + failed = await store.applyIncomingBatch(records, countTelemetry); + Assert.equal(0, failed.length); + Assert.equal(0, countTelemetry.incomingCounts.failed); + addon = await AddonManager.getAddonByID(ID3); + Assert.ok(!addon.userDisabled); + await checkReconcilerUpToDate(addon); + records = []; + + await uninstallAddon(addon, reconciler); +}); + +add_task(async function test_ignore_different_appid() { + _( + "Ensure that incoming records with a different application ID are ignored." + ); + + // We test by creating a record that should result in an update. + let addon = await installAddon(XPIS.test_addon1, reconciler); + Assert.ok(!addon.userDisabled); + + let record = createRecordForThisApp(addon.syncGUID, ID1, false, false); + record.applicationID = "FAKE_ID"; + let countTelemetry = new SyncedRecordsTelemetry(); + let failed = await store.applyIncomingBatch([record], countTelemetry); + Assert.equal(0, failed.length); + + let newAddon = await AddonManager.getAddonByID(ID1); + Assert.ok(!newAddon.userDisabled); + + await uninstallAddon(addon, reconciler); +}); + +add_task(async function test_ignore_unknown_source() { + _("Ensure incoming records with unknown source are ignored."); + + let addon = await installAddon(XPIS.test_addon1, reconciler); + + let record = createRecordForThisApp(addon.syncGUID, ID1, false, false); + record.source = "DUMMY_SOURCE"; + let countTelemetry = new SyncedRecordsTelemetry(); + let failed = await store.applyIncomingBatch([record], countTelemetry); + Assert.equal(0, failed.length); + + let newAddon = await AddonManager.getAddonByID(ID1); + Assert.ok(!newAddon.userDisabled); + + await uninstallAddon(addon, reconciler); +}); + +add_task(async function test_apply_uninstall() { + _("Ensures that uninstalling an add-on from a record works."); + + let addon = await installAddon(XPIS.test_addon1, reconciler); + + let records = []; + let countTelemetry = new SyncedRecordsTelemetry(); + records.push(createRecordForThisApp(addon.syncGUID, ID1, true, true)); + let failed = await store.applyIncomingBatch(records, countTelemetry); + Assert.equal(0, failed.length); + Assert.equal(0, countTelemetry.incomingCounts.failed); + + addon = await AddonManager.getAddonByID(ID1); + Assert.equal(null, addon); +}); + +add_task(async function test_addon_syncability() { + _("Ensure isAddonSyncable functions properly."); + + Svc.Prefs.set( + "addons.trustedSourceHostnames", + "addons.mozilla.org,other.example.com" + ); + + Assert.ok(!(await store.isAddonSyncable(null))); + + let addon = await installAddon(XPIS.test_addon1, reconciler); + Assert.ok(await store.isAddonSyncable(addon)); + + let dummy = {}; + const KEYS = [ + "id", + "syncGUID", + "type", + "scope", + "foreignInstall", + "isSyncable", + ]; + for (let k of KEYS) { + dummy[k] = addon[k]; + } + + Assert.ok(await store.isAddonSyncable(dummy)); + + dummy.type = "UNSUPPORTED"; + Assert.ok(!(await store.isAddonSyncable(dummy))); + dummy.type = addon.type; + + dummy.scope = 0; + Assert.ok(!(await store.isAddonSyncable(dummy))); + dummy.scope = addon.scope; + + dummy.isSyncable = false; + Assert.ok(!(await store.isAddonSyncable(dummy))); + dummy.isSyncable = addon.isSyncable; + + dummy.foreignInstall = true; + Assert.ok(!(await store.isAddonSyncable(dummy))); + dummy.foreignInstall = false; + + await uninstallAddon(addon, reconciler); + + Assert.ok(!store.isSourceURITrusted(null)); + + let trusted = [ + "https://addons.mozilla.org/foo", + "https://other.example.com/foo", + ]; + + let untrusted = [ + "http://addons.mozilla.org/foo", // non-https + "ftps://addons.mozilla.org/foo", // non-https + "https://untrusted.example.com/foo", // non-trusted hostname` + ]; + + for (let uri of trusted) { + Assert.ok(store.isSourceURITrusted(Services.io.newURI(uri))); + } + + for (let uri of untrusted) { + Assert.ok(!store.isSourceURITrusted(Services.io.newURI(uri))); + } + + Svc.Prefs.set("addons.trustedSourceHostnames", ""); + for (let uri of trusted) { + Assert.ok(!store.isSourceURITrusted(Services.io.newURI(uri))); + } + + Svc.Prefs.set("addons.trustedSourceHostnames", "addons.mozilla.org"); + Assert.ok( + store.isSourceURITrusted( + Services.io.newURI("https://addons.mozilla.org/foo") + ) + ); + + Svc.Prefs.reset("addons.trustedSourceHostnames"); +}); + +add_task(async function test_get_all_ids() { + _("Ensures that getAllIDs() returns an appropriate set."); + + _("Installing two addons."); + // XXX - this test seems broken - at this point, before we've installed the + // addons below, store.getAllIDs() returns all addons installed by previous + // tests, even though those tests uninstalled the addon. + // So if any tests above ever add a new addon ID, they are going to need to + // be added here too. + // Assert.equal(0, Object.keys(store.getAllIDs()).length); + let addon1 = await installAddon(XPIS.test_addon1, reconciler); + let addon2 = await installAddon(XPIS.test_addon2, reconciler); + let addon3 = await installAddon(XPIS.test_addon3, reconciler); + + _("Ensure they're syncable."); + Assert.ok(await store.isAddonSyncable(addon1)); + Assert.ok(await store.isAddonSyncable(addon2)); + Assert.ok(await store.isAddonSyncable(addon3)); + + let ids = await store.getAllIDs(); + + Assert.equal("object", typeof ids); + Assert.equal(3, Object.keys(ids).length); + Assert.ok(addon1.syncGUID in ids); + Assert.ok(addon2.syncGUID in ids); + Assert.ok(addon3.syncGUID in ids); + + await uninstallAddon(addon1, reconciler); + await uninstallAddon(addon2, reconciler); + await uninstallAddon(addon3, reconciler); +}); + +add_task(async function test_change_item_id() { + _("Ensures that changeItemID() works properly."); + + let addon = await installAddon(XPIS.test_addon1, reconciler); + + let oldID = addon.syncGUID; + let newID = Utils.makeGUID(); + + await store.changeItemID(oldID, newID); + + let newAddon = await AddonManager.getAddonByID(ID1); + Assert.notEqual(null, newAddon); + Assert.equal(newID, newAddon.syncGUID); + + await uninstallAddon(newAddon, reconciler); +}); + +add_task(async function test_create() { + _("Ensure creating/installing an add-on from a record works."); + + let server = createAndStartHTTPServer(HTTP_PORT); + + let guid = Utils.makeGUID(); + let record = createRecordForThisApp(guid, ID1, true, false); + let countTelemetry = new SyncedRecordsTelemetry(); + let failed = await store.applyIncomingBatch([record], countTelemetry); + Assert.equal(0, failed.length); + + let newAddon = await AddonManager.getAddonByID(ID1); + Assert.notEqual(null, newAddon); + Assert.equal(guid, newAddon.syncGUID); + Assert.ok(!newAddon.userDisabled); + + await uninstallAddon(newAddon, reconciler); + + await promiseStopServer(server); +}); + +add_task(async function test_create_missing_search() { + _("Ensures that failed add-on searches are handled gracefully."); + + let server = createAndStartHTTPServer(HTTP_PORT); + + // The handler for this ID is not installed, so a search should 404. + const id = "missing@tests.mozilla.org"; + let guid = Utils.makeGUID(); + let record = createRecordForThisApp(guid, id, true, false); + let countTelemetry = new SyncedRecordsTelemetry(); + let failed = await store.applyIncomingBatch([record], countTelemetry); + Assert.equal(1, failed.length); + Assert.equal(guid, failed[0]); + Assert.equal( + countTelemetry.incomingCounts.failedReasons[0].name, + "GET <URL> failed (status 404)" + ); + Assert.equal(countTelemetry.incomingCounts.failedReasons[0].count, 1); + + let addon = await AddonManager.getAddonByID(id); + Assert.equal(null, addon); + + await promiseStopServer(server); +}); + +add_task(async function test_create_bad_install() { + _("Ensures that add-ons without a valid install are handled gracefully."); + + let server = createAndStartHTTPServer(HTTP_PORT); + + // The handler returns a search result but the XPI will 404. + const id = "missing-xpi@tests.mozilla.org"; + let guid = Utils.makeGUID(); + let record = createRecordForThisApp(guid, id, true, false); + let countTelemetry = new SyncedRecordsTelemetry(); + /* let failed = */ await store.applyIncomingBatch([record], countTelemetry); + // This addon had no source URI so was skipped - but it's not treated as + // failure. + // XXX - this test isn't testing what we thought it was. Previously the addon + // was not being installed due to requireSecureURL checking *before* we'd + // attempted to get the XPI. + // With requireSecureURL disabled we do see a download failure, but the addon + // *does* get added to |failed|. + // FTR: onDownloadFailed() is called with ERROR_NETWORK_FAILURE, so it's going + // to be tricky to distinguish a 404 from other transient network errors + // where we do want the addon to end up in |failed|. + // This is being tracked in bug 1284778. + // Assert.equal(0, failed.length); + + let addon = await AddonManager.getAddonByID(id); + Assert.equal(null, addon); + + await promiseStopServer(server); +}); + +add_task(async function test_ignore_system() { + _("Ensure we ignore system addons"); + // Our system addon should not appear in getAllIDs + await engine._refreshReconcilerState(); + let num = 0; + let ids = await store.getAllIDs(); + for (let guid in ids) { + num += 1; + let addon = reconciler.getAddonStateFromSyncGUID(guid); + Assert.notEqual(addon.id, SYSTEM_ADDON_ID); + } + Assert.greater(num, 1, "should have seen at least one."); +}); + +add_task(async function test_incoming_system() { + _("Ensure we handle incoming records that refer to a system addon"); + // eg, loop initially had a normal addon but it was then "promoted" to be a + // system addon but wanted to keep the same ID. The server record exists due + // to this. + + // before we start, ensure the system addon isn't disabled. + Assert.ok(!(await AddonManager.getAddonByID(SYSTEM_ADDON_ID).userDisabled)); + + // Now simulate an incoming record with the same ID as the system addon, + // but flagged as disabled - it should not be applied. + let server = createAndStartHTTPServer(HTTP_PORT); + // We make the incoming record flag the system addon as disabled - it should + // be ignored. + let guid = Utils.makeGUID(); + let record = createRecordForThisApp(guid, SYSTEM_ADDON_ID, false, false); + let countTelemetry = new SyncedRecordsTelemetry(); + let failed = await store.applyIncomingBatch([record], countTelemetry); + Assert.equal(0, failed.length); + + // The system addon should still not be userDisabled. + Assert.ok(!(await AddonManager.getAddonByID(SYSTEM_ADDON_ID).userDisabled)); + + await promiseStopServer(server); +}); + +add_task(async function test_wipe() { + _("Ensures that wiping causes add-ons to be uninstalled."); + + await installAddon(XPIS.test_addon1, reconciler); + + await store.wipe(); + + let addon = await AddonManager.getAddonByID(ID1); + Assert.equal(null, addon); +}); + +add_task(async function test_wipe_and_install() { + _("Ensure wipe followed by install works."); + + // This tests the reset sync flow where remote data is replaced by local. The + // receiving client will see a wipe followed by a record which should undo + // the wipe. + let installed = await installAddon(XPIS.test_addon1, reconciler); + + let record = createRecordForThisApp(installed.syncGUID, ID1, true, false); + + await store.wipe(); + + let deleted = await AddonManager.getAddonByID(ID1); + Assert.equal(null, deleted); + + // Re-applying the record can require re-fetching the XPI. + let server = createAndStartHTTPServer(HTTP_PORT); + + await store.applyIncoming(record); + + let fetched = await AddonManager.getAddonByID(record.addonID); + Assert.ok(!!fetched); + + // wipe again to we are left with a clean slate. + await store.wipe(); + + await promiseStopServer(server); +}); + +// STR for what this is testing: +// * Either: +// * Install then remove an addon, then delete addons.json from the profile +// or corrupt it (in which case the addon manager will remove it) +// * Install then remove an addon while addon caching is disabled, then +// re-enable addon caching. +// * Install the same addon in a different profile, sync it. +// * Sync this profile +// Before bug 1467904, the addon would fail to install because this profile +// has a copy of the addon in our addonsreconciler.json, but the addon manager +// does *not* have a copy in its cache, and repopulating that cache would not +// re-add it as the addon is no longer installed locally. +add_task(async function test_incoming_reconciled_but_not_cached() { + _( + "Ensure we handle incoming records our reconciler has but the addon cache does not" + ); + + // Make sure addon is not installed. + let addon = await AddonManager.getAddonByID(ID1); + Assert.equal(null, addon); + + Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", false); + + addon = await installAddon(XPIS.test_addon1, reconciler); + Assert.notEqual(await AddonManager.getAddonByID(ID1), null); + await uninstallAddon(addon, reconciler); + + Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", true); + + // now pretend it is incoming. + let server = createAndStartHTTPServer(HTTP_PORT); + let guid = Utils.makeGUID(); + let record = createRecordForThisApp(guid, ID1, true, false); + let countTelemetry = new SyncedRecordsTelemetry(); + let failed = await store.applyIncomingBatch([record], countTelemetry); + Assert.equal(0, failed.length); + + Assert.notEqual(await AddonManager.getAddonByID(ID1), null); + + await promiseStopServer(server); +}); + +// NOTE: The test above must be the last test run due to the addon cache +// being trashed. It is probably possible to fix that by running, eg, +// AddonRespository.backgroundUpdateCheck() to rebuild the cache, but that +// requires implementing more AMO functionality in our test server + +add_task(async function cleanup() { + // There's an xpcom-shutdown hook for this, but let's give this a shot. + reconciler.stopListening(); +}); |