test_BackupService_regeneration.js (20644B)
1 /* Any copyright is dedicated to the Public Domain. 2 https://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const { NetUtil } = ChromeUtils.importESModule( 7 "resource://gre/modules/NetUtil.sys.mjs" 8 ); 9 const { PlacesTestUtils } = ChromeUtils.importESModule( 10 "resource://testing-common/PlacesTestUtils.sys.mjs" 11 ); 12 const { PlacesUtils } = ChromeUtils.importESModule( 13 "resource://gre/modules/PlacesUtils.sys.mjs" 14 ); 15 const { TestUtils } = ChromeUtils.importESModule( 16 "resource://testing-common/TestUtils.sys.mjs" 17 ); 18 const { DownloadHistory } = ChromeUtils.importESModule( 19 "resource://gre/modules/DownloadHistory.sys.mjs" 20 ); 21 const { AddonTestUtils } = ChromeUtils.importESModule( 22 "resource://testing-common/AddonTestUtils.sys.mjs" 23 ); 24 const { ExtensionTestUtils } = ChromeUtils.importESModule( 25 "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" 26 ); 27 const { formAutofillStorage } = ChromeUtils.importESModule( 28 "resource://autofill/FormAutofillStorage.sys.mjs" 29 ); 30 const { Sanitizer } = ChromeUtils.importESModule( 31 "resource:///modules/Sanitizer.sys.mjs" 32 ); 33 const { NewTabUtils } = ChromeUtils.importESModule( 34 "resource://gre/modules/NewTabUtils.sys.mjs" 35 ); 36 const { CookieXPCShellUtils } = ChromeUtils.importESModule( 37 "resource://testing-common/CookieXPCShellUtils.sys.mjs" 38 ); 39 40 ExtensionTestUtils.init(this); 41 AddonTestUtils.init(this); 42 AddonTestUtils.createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1"); 43 44 const nsLoginInfo = new Components.Constructor( 45 "@mozilla.org/login-manager/loginInfo;1", 46 Ci.nsILoginInfo, 47 "init" 48 ); 49 50 /** 51 * This suite of tests ensures that the current backup will be deleted, and 52 * a new one generated when certain user actions occur. 53 */ 54 55 /** 56 * Adds a history visit to the Places database. 57 * 58 * @param {string} uriString 59 * A string representation of the URI to add a visit for. 60 * @param {number} [timestamp=Date.now()] 61 * The timestamp for the visit to be used. By default, this is the current 62 * date and time. 63 * @returns {Promise<undefined>} 64 */ 65 async function addTestHistory(uriString, timestamp = Date.now()) { 66 let uri = NetUtil.newURI(uriString); 67 await PlacesTestUtils.addVisits({ 68 uri, 69 transition: Ci.nsINavHistoryService.TRANSITION_TYPED, 70 visitDate: timestamp * 1000, 71 }); 72 } 73 74 let gCookieCounter = 0; 75 76 const COOKIE_HOST = "example.com"; 77 const COOKIE_PATH = "/"; 78 const COOKIE_ORIGIN_ATTRIBUTES = Object.freeze({}); 79 80 /** 81 * Adds a new non-session cookie to the cookie database with the host set 82 * as COOKIE_HOST, the path as COOKIE_PATH, with the origin attributes of 83 * COOKIE_ORIGIN_ATTRIBUTES and a generated name and value. 84 * 85 * @param {boolean} isSessionCookie 86 * True if the cookie should be a session cookie. 87 * @returns {string} 88 * The name of the cookie that was generated. 89 */ 90 function addTestCookie(isSessionCookie) { 91 gCookieCounter++; 92 let name = `Cookie name: ${gCookieCounter}`; 93 94 const cv = Services.cookies.add( 95 COOKIE_HOST, 96 COOKIE_PATH, 97 name, 98 `Cookie value: ${gCookieCounter}`, 99 false, 100 false, 101 isSessionCookie, 102 Date.now() + 1000, 103 COOKIE_ORIGIN_ATTRIBUTES, 104 Ci.nsICookie.SAMESITE_UNSET, 105 Ci.nsICookie.SCHEME_HTTP 106 ); 107 Assert.equal(cv.result, Ci.nsICookieValidation.eOK); 108 109 return name; 110 } 111 112 /** 113 * A helper function that sets up a BackupService to be instrumented to detect 114 * a backup regeneration, and then runs an async taskFn to ensure that the 115 * regeneration occurs. 116 * 117 * @param {Function} taskFn 118 * The async function to run after the BackupService has been set up. It is 119 * not passed any arguments. 120 * @param {string} msg 121 * The message to write in the assertion when the regeneration occurs. 122 */ 123 async function expectRegeneration(taskFn, msg) { 124 let sandbox = sinon.createSandbox(); 125 // Override the default REGENERATION_DEBOUNCE_RATE_MS to 0 so that we don't 126 // have to wait long for the debouncer to fire. We need to do this before 127 // we construct the BackupService that we'll use for the test. 128 sandbox.stub(BackupService, "REGENERATION_DEBOUNCE_RATE_MS").get(() => 0); 129 130 let bs = new BackupService(); 131 132 // Now we set up a stub on the BackupService to detect calls to 133 // createbackupOnIdleDispatch 134 let createBackupDeferred = Promise.withResolvers(); 135 sandbox.stub(bs, "createBackupOnIdleDispatch").callsFake(options => { 136 Assert.ok(true, "Saw createBackupOnIdleDispatch call"); 137 Assert.equal( 138 options.reason, 139 "user deleted some data", 140 "Backup was recorded as being caused by user data deletion" 141 ); 142 createBackupDeferred.resolve(); 143 return Promise.resolve(); 144 }); 145 146 // Creating a new backup will only occur if scheduled backups are enabled, 147 // so let's set the pref... 148 Services.prefs.setBoolPref("browser.backup.scheduled.enabled", true); 149 Services.prefs.setBoolPref("browser.backup.archive.enabled", true); 150 // But also stub out `onIdle` so that we don't get any interference during 151 // our test by the idle service. 152 sandbox.stub(bs, "onIdle").returns(); 153 154 bs.initBackupScheduler(); 155 156 await taskFn(); 157 158 // We'll wait for 1 second before considering the regeneration a bust. 159 let timeoutPromise = new Promise((resolve, reject) => 160 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 161 setTimeout(() => { 162 reject(); 163 }, 1000) 164 ); 165 166 try { 167 await Promise.race([createBackupDeferred.promise, timeoutPromise]); 168 Assert.ok(true, msg); 169 } catch (e) { 170 Assert.ok(false, "Timed out waiting for regeneration."); 171 } 172 173 bs.uninitBackupScheduler(); 174 Services.prefs.clearUserPref("browser.backup.scheduled.enabled"); 175 sandbox.restore(); 176 } 177 178 /** 179 * A helper function that sets up a BackupService to be instrumented to detect 180 * a backup regeneration, and then runs an async taskFn to ensure that the 181 * regeneration DOES NOT occur. 182 * 183 * @param {Function} taskFn 184 * The async function to run after the BackupService has been set up. It is 185 * not passed any arguments. 186 * @param {string} msg 187 * The message to write in the assertion when the regeneration does not occur 188 * within the timeout. 189 */ 190 async function expectNoRegeneration(taskFn, msg) { 191 let sandbox = sinon.createSandbox(); 192 // Override the default REGENERATION_DEBOUNCE_RATE_MS to 0 so that we don't 193 // have to wait long for the debouncer to fire. We need to do this before 194 // we construct the BackupService that we'll use for the test. 195 sandbox.stub(BackupService, "REGENERATION_DEBOUNCE_RATE_MS").get(() => 0); 196 197 let bs = new BackupService(); 198 199 // Now we set up some stubs on the BackupService to detect calls to 200 // deleteLastBackup and createbackupOnIdleDispatch, which are both called 201 // on regeneration. We share the same Promise here because in either of these 202 // being called, this test is a failure. 203 let regenerationPromise = Promise.withResolvers(); 204 sandbox.stub(bs, "deleteLastBackup").callsFake(() => { 205 Assert.ok(false, "Unexpectedly saw deleteLastBackup call"); 206 regenerationPromise.reject(); 207 return Promise.resolve(); 208 }); 209 210 sandbox.stub(bs, "createBackupOnIdleDispatch").callsFake(() => { 211 Assert.ok(false, "Unexpectedly saw createBackupOnIdleDispatch call"); 212 regenerationPromise.reject(); 213 return Promise.resolve(); 214 }); 215 216 // Creating a new backup will only occur if scheduled backups are enabled, 217 // so let's set the pref... 218 Services.prefs.setBoolPref("browser.backup.scheduled.enabled", true); 219 // But also stub out `onIdle` so that we don't get any interference during 220 // our test by the idle service. 221 sandbox.stub(bs, "onIdle").resolves(); 222 223 bs.initBackupScheduler(); 224 225 await taskFn(); 226 227 // We'll wait for 1 second, and if we don't see the regeneration attempt, 228 // we'll assume it's not coming. 229 let timeoutPromise = new Promise(resolve => 230 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 231 setTimeout(() => { 232 Assert.ok(true, "Saw no regeneration within 1 second."); 233 resolve(); 234 }, 1000) 235 ); 236 237 try { 238 await Promise.race([regenerationPromise.promise, timeoutPromise]); 239 Assert.ok(true, msg); 240 } catch (e) { 241 Assert.ok(false, "Saw an unexpected regeneration."); 242 } 243 244 bs.uninitBackupScheduler(); 245 Services.prefs.clearUserPref("browser.backup.scheduled.enabled"); 246 sandbox.restore(); 247 } 248 249 add_setup(() => { 250 CookieXPCShellUtils.createServer({ hosts: ["example.com"] }); 251 Services.prefs.setBoolPref("dom.security.https_first", false); 252 253 // Allow all cookies. 254 Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); 255 Services.prefs.setBoolPref( 256 "network.cookieJarSettings.unblocked_for_testing", 257 true 258 ); 259 }); 260 261 /** 262 * Tests that backup regeneration occurs on the page-removed PlacesObserver 263 * event that indicates manual user deletion of a page from history. 264 */ 265 add_task(async function test_page_removed_reason_deleted() { 266 const PAGE_URI = "https://test.com"; 267 await addTestHistory(PAGE_URI); 268 await expectRegeneration(async () => { 269 await PlacesUtils.history.remove(PAGE_URI); 270 }, "Saw regeneration on page-removed via user deletion."); 271 }); 272 273 /** 274 * Tests that backup regeneration does not occur on the page-removed 275 * PlacesObserver event that indicates automatic deletion of a page from 276 * history. 277 */ 278 add_task(async function test_page_removed_reason_expired() { 279 const PAGE_URI = "https://test.com"; 280 await addTestHistory( 281 PAGE_URI, 282 0 /* Timestamp at 0 is wayyyyy in the past, in the 1960's - it's the UNIX epoch start date */ 283 ); 284 await expectNoRegeneration(async () => { 285 // This is how the Places tests force expiration, so we'll do it the same 286 // way. 287 let promise = TestUtils.topicObserved( 288 PlacesUtils.TOPIC_EXPIRATION_FINISHED 289 ); 290 let expire = Cc["@mozilla.org/places/expiration;1"].getService( 291 Ci.nsIObserver 292 ); 293 expire.observe(null, "places-debug-start-expiration", -1); 294 await promise; 295 }, "Saw no regeneration on page-removed via expiration."); 296 }); 297 298 /** 299 * Tests that backup regeneration occurs on the history-cleared PlacesObserver 300 * event that indicates clearing of all user history. 301 */ 302 add_task(async function test_history_cleared() { 303 const PAGE_URI = "https://test.com"; 304 await addTestHistory(PAGE_URI); 305 await expectRegeneration(async () => { 306 await PlacesUtils.history.clear(); 307 }, "Saw regeneration on history-cleared."); 308 }); 309 310 /** 311 * Tests that backup regeneration occurs when removing a download from history. 312 */ 313 add_task(async function test_download_removed() { 314 const FAKE_FILE_PATH = PathUtils.join(PathUtils.tempDir, "somefile.zip"); 315 let download = { 316 source: { 317 url: "https://test.com/somefile", 318 isPrivate: false, 319 }, 320 target: { path: FAKE_FILE_PATH }, 321 }; 322 await DownloadHistory.addDownloadToHistory(download); 323 324 await expectRegeneration(async () => { 325 await PlacesUtils.history.remove(download.source.url); 326 }, "Saw regeneration on download removal."); 327 }); 328 329 /** 330 * Tests that backup regeneration occurs when clearing all downloads. 331 */ 332 add_task(async function test_all_downloads_removed() { 333 const FAKE_FILE_PATH = PathUtils.join(PathUtils.tempDir, "somefile.zip"); 334 let download1 = { 335 source: { 336 url: "https://test.com/somefile", 337 isPrivate: false, 338 }, 339 target: { path: FAKE_FILE_PATH }, 340 }; 341 let download2 = { 342 source: { 343 url: "https://test.com/somefile2", 344 isPrivate: false, 345 }, 346 target: { path: FAKE_FILE_PATH }, 347 }; 348 await DownloadHistory.addDownloadToHistory(download1); 349 await DownloadHistory.addDownloadToHistory(download2); 350 351 await expectRegeneration(async () => { 352 await PlacesUtils.history.removeVisitsByFilter({ 353 transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD, 354 }); 355 }, "Saw regeneration on all downloads removed."); 356 }); 357 358 /** 359 * Tests that backup regeneration occurs when removing a password. 360 */ 361 add_task(async function test_password_removed() { 362 let login = new nsLoginInfo( 363 "https://example.com", 364 "https://example.com", 365 null, 366 "notifyu1", 367 "notifyp1", 368 "user", 369 "pass" 370 ); 371 await Services.logins.addLoginAsync(login); 372 373 await expectRegeneration(async () => { 374 Services.logins.removeLogin(login); 375 }, "Saw regeneration on password removed."); 376 }); 377 378 /** 379 * Tests that backup regeneration occurs when all passwords are removed. 380 */ 381 add_task(async function test_all_passwords_removed() { 382 let login1 = new nsLoginInfo( 383 "https://example.com", 384 "https://example.com", 385 null, 386 "notifyu1", 387 "notifyp1", 388 "user", 389 "pass" 390 ); 391 let login2 = new nsLoginInfo( 392 "https://example.com", 393 "https://example.com", 394 null, 395 "", 396 "notifyp1", 397 "", 398 "pass" 399 ); 400 401 await Services.logins.addLoginAsync(login1); 402 await Services.logins.addLoginAsync(login2); 403 404 await expectRegeneration(async () => { 405 Services.logins.removeAllLogins(); 406 }, "Saw regeneration on all passwords removed."); 407 }); 408 409 /** 410 * Tests that backup regeneration occurs when removing a bookmark. 411 */ 412 add_task(async function test_bookmark_removed() { 413 let bookmark = await PlacesUtils.bookmarks.insert({ 414 parentGuid: PlacesUtils.bookmarks.unfiledGuid, 415 url: "data:text/plain,Content", 416 title: "Regeneration Test Bookmark", 417 }); 418 419 await expectRegeneration(async () => { 420 await PlacesUtils.bookmarks.remove(bookmark); 421 }, "Saw regeneration on bookmark removed."); 422 }); 423 424 /** 425 * Tests that backup regeneration occurs when all bookmarks are removed. 426 */ 427 add_task(async function test_all_bookmarks_removed() { 428 await PlacesUtils.bookmarks.insert({ 429 parentGuid: PlacesUtils.bookmarks.unfiledGuid, 430 url: "data:text/plain,Content", 431 title: "Regeneration Test Bookmark 1", 432 }); 433 await PlacesUtils.bookmarks.insert({ 434 parentGuid: PlacesUtils.bookmarks.unfiledGuid, 435 url: "data:text/plain,Content2", 436 title: "Regeneration Test Bookmark 2", 437 }); 438 439 await expectRegeneration(async () => { 440 await PlacesUtils.bookmarks.eraseEverything(); 441 }, "Saw regeneration on all bookmarks removed."); 442 }); 443 444 /** 445 * Tests that backup regeneration occurs when an addon is uninstalled. 446 */ 447 add_task(async function test_addon_uninstalled() { 448 await AddonTestUtils.promiseStartupManager(); 449 450 let testExtension = ExtensionTestUtils.loadExtension({ 451 manifest: { 452 name: "Some test extension", 453 browser_specific_settings: { 454 gecko: { id: "test-backup-regeneration@ext-0" }, 455 }, 456 }, 457 useAddonManager: "temporary", 458 }); 459 460 await testExtension.startup(); 461 462 await expectRegeneration(async () => { 463 await testExtension.unload(); 464 }, "Saw regeneration on test extension uninstall."); 465 }); 466 467 /** 468 * Tests that backup regeneration occurs when removing a payment method. 469 */ 470 add_task(async function test_payment_method_removed() { 471 await formAutofillStorage.initialize(); 472 let guid = await formAutofillStorage.creditCards.add({ 473 "cc-name": "Foxy the Firefox", 474 "cc-number": "5555555555554444", 475 "cc-exp-month": 5, 476 "cc-exp-year": 2099, 477 }); 478 479 await expectRegeneration(async () => { 480 await formAutofillStorage.creditCards.remove(guid); 481 }, "Saw regeneration on payment method removal."); 482 }); 483 484 /** 485 * Tests that backup regeneration occurs when removing an address. 486 */ 487 add_task(async function test_address_removed() { 488 await formAutofillStorage.initialize(); 489 let guid = await formAutofillStorage.addresses.add({ 490 "given-name": "John", 491 "additional-name": "R.", 492 "family-name": "Smith", 493 organization: "World Wide Web Consortium", 494 "street-address": "32 Vassar Street\\\nMIT Room 32-G524", 495 "address-level2": "Cambridge", 496 "address-level1": "MA", 497 "postal-code": "02139", 498 country: "US", 499 tel: "+15195555555", 500 email: "user@example.com", 501 }); 502 503 await expectRegeneration(async () => { 504 await formAutofillStorage.addresses.remove(guid); 505 }, "Saw regeneration on address removal."); 506 }); 507 508 /** 509 * Tests that backup regeneration occurs after any kind of data sanitization. 510 */ 511 add_task(async function test_sanitization() { 512 await expectRegeneration(async () => { 513 await Sanitizer.sanitize(["cookiesAndStorage"]); 514 }, "Saw regeneration on sanitization of cookies and storage."); 515 516 await expectRegeneration(async () => { 517 await Sanitizer.sanitize(["siteSettings"]); 518 }, "Saw regeneration on sanitization of site settings."); 519 }); 520 521 /** 522 * Tests that backup regeneration occurs after a permission is removed. 523 */ 524 add_task(async function test_permission_removed() { 525 let principal = 526 Services.scriptSecurityManager.createContentPrincipalFromOrigin( 527 "https://test-permission-site.com" 528 ); 529 const PERMISSION_TYPE = "desktop-notification"; 530 Services.perms.addFromPrincipal( 531 principal, 532 PERMISSION_TYPE, 533 Services.perms.ALLOW_ACTION 534 ); 535 536 await expectRegeneration(async () => { 537 Services.perms.removeFromPrincipal(principal, PERMISSION_TYPE); 538 }, "Saw regeneration on permission removal."); 539 }); 540 541 /** 542 * Tests that backup regeneration occurs when persistent and session cookies are 543 * removed. 544 */ 545 add_task(async function test_cookies_removed() { 546 for (let isSessionCookie of [false, true]) { 547 Services.cookies.removeAll(); 548 549 // First, let's remove a single cookie by host, path, name and origin 550 // attrbutes. 551 let name = addTestCookie(isSessionCookie); 552 553 if (isSessionCookie) { 554 Assert.equal( 555 Services.cookies.sessionCookies.length, 556 1, 557 "Make sure we actually added a session cookie." 558 ); 559 } else { 560 Assert.equal( 561 Services.cookies.sessionCookies.length, 562 0, 563 "Make sure we actually added a persistent cookie." 564 ); 565 } 566 567 await expectRegeneration(async () => { 568 Services.cookies.remove( 569 COOKIE_HOST, 570 name, 571 COOKIE_PATH, 572 COOKIE_ORIGIN_ATTRIBUTES 573 ); 574 }, "Saw regeneration on single cookie removal."); 575 576 // Now remove all cookies for a particular host. 577 addTestCookie(isSessionCookie); 578 addTestCookie(isSessionCookie); 579 580 await expectRegeneration(async () => { 581 Services.cookies.removeCookiesFromExactHost(COOKIE_HOST, "{}"); 582 }, "Saw regeneration on cookie removal by host."); 583 584 // Now remove all cookies. 585 const COOKIES_TO_ADD = 10; 586 for (let i = 0; i < COOKIES_TO_ADD; ++i) { 587 addTestCookie(isSessionCookie); 588 } 589 590 await expectRegeneration(async () => { 591 Services.cookies.removeAll(); 592 }, "Saw regeneration on all cookie removal."); 593 } 594 }); 595 596 /** 597 * Tests that backup regeneration does not occur if a cookie is removed due 598 * to expiry. 599 */ 600 add_task(async function test_cookies_not_removed_expiry() { 601 await expectNoRegeneration(async () => { 602 const COOKIE = "test=test"; 603 await CookieXPCShellUtils.setCookieToDocument( 604 "http://example.com/", 605 COOKIE 606 ); 607 608 // Now expire that cookie by using the same value, but setting the expires 609 // directive to sometime wayyyyy in the past. 610 const EXPIRE = `${COOKIE}; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; 611 await CookieXPCShellUtils.setCookieToDocument( 612 "http://example.com/", 613 EXPIRE 614 ); 615 }, "Saw no regeneration on cookie expiry."); 616 }); 617 618 /** 619 * Tests that backup regeneration occurs when newtab links are blocked. 620 */ 621 add_task(async function test_newtab_link_blocked() { 622 NewTabUtils.init(); 623 624 await expectRegeneration(async () => { 625 NewTabUtils.activityStreamLinks.blockURL("https://example.com"); 626 }, "Saw regeneration on the blocking of a newtab link"); 627 }); 628 629 /** 630 * Tests that backup regeneration occurs when sanitizeOnShutdown is enabled 631 * or disabled 632 */ 633 add_task(async function test_sanitizeOnShutdown_regeneration() { 634 await expectRegeneration(() => { 635 Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", true); 636 }, "Saw a regeneration when sanitizeOnShutdown was enabled"); 637 638 await expectRegeneration(() => { 639 Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", false); 640 }, "Saw a regeneration when sanitizeOnShutdown was disabled"); 641 642 Services.prefs.clearUserPref("privacy.sanitize.sanitizeOnShutdown"); 643 }); 644 645 /** 646 * Tests that setting up a regeneration without archiveEnabledStatus being true 647 * leads to NO regeneration upon data mutation 648 */ 649 add_task(async function test_enabledStatus_no_regeneration() { 650 let bookmark = await PlacesUtils.bookmarks.insert({ 651 parentGuid: PlacesUtils.bookmarks.unfiledGuid, 652 url: "data:text/plain,Content", 653 title: "Regeneration Test Bookmark", 654 }); 655 656 Services.prefs.setBoolPref("browser.backup.archive.enabled", false); 657 658 await expectNoRegeneration(async () => { 659 await PlacesUtils.bookmarks.remove(bookmark); 660 }, "Saw no regeneration on bookmark removed."); 661 662 Services.prefs.clearUserPref("browser.backup.archive.enabled"); 663 });