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 });