/* Any copyright is dedicated to the Public Domain. https://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { setTimeout } = ChromeUtils.importESModule( "resource://gre/modules/Timer.sys.mjs" ); const { NetUtil } = ChromeUtils.importESModule( "resource://gre/modules/NetUtil.sys.mjs" ); const { PlacesTestUtils } = ChromeUtils.importESModule( "resource://testing-common/PlacesTestUtils.sys.mjs" ); const { PlacesUtils } = ChromeUtils.importESModule( "resource://gre/modules/PlacesUtils.sys.mjs" ); const { TestUtils } = ChromeUtils.importESModule( "resource://testing-common/TestUtils.sys.mjs" ); const { DownloadHistory } = ChromeUtils.importESModule( "resource://gre/modules/DownloadHistory.sys.mjs" ); const { AddonTestUtils } = ChromeUtils.importESModule( "resource://testing-common/AddonTestUtils.sys.mjs" ); const { ExtensionTestUtils } = ChromeUtils.importESModule( "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" ); const { formAutofillStorage } = ChromeUtils.importESModule( "resource://autofill/FormAutofillStorage.sys.mjs" ); const { Sanitizer } = ChromeUtils.importESModule( "resource:///modules/Sanitizer.sys.mjs" ); const { NewTabUtils } = ChromeUtils.importESModule( "resource://gre/modules/NewTabUtils.sys.mjs" ); const { CookieXPCShellUtils } = ChromeUtils.importESModule( "resource://testing-common/CookieXPCShellUtils.sys.mjs" ); ExtensionTestUtils.init(this); AddonTestUtils.init(this); AddonTestUtils.createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1"); const nsLoginInfo = new Components.Constructor( "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init" ); /** * This suite of tests ensures that the current backup will be deleted, and * a new one generated when certain user actions occur. */ /** * Adds a history visit to the Places database. * * @param {string} uriString * A string representation of the URI to add a visit for. * @param {number} [timestamp=Date.now()] * The timestamp for the visit to be used. By default, this is the current * date and time. * @returns {Promise} */ async function addTestHistory(uriString, timestamp = Date.now()) { let uri = NetUtil.newURI(uriString); await PlacesTestUtils.addVisits({ uri, transition: Ci.nsINavHistoryService.TRANSITION_TYPED, visitDate: timestamp * 1000, }); } let gCookieCounter = 0; const COOKIE_HOST = "example.com"; const COOKIE_PATH = "/"; const COOKIE_ORIGIN_ATTRIBUTES = Object.freeze({}); /** * Adds a new non-session cookie to the cookie database with the host set * as COOKIE_HOST, the path as COOKIE_PATH, with the origin attributes of * COOKIE_ORIGIN_ATTRIBUTES and a generated name and value. * * @param {boolean} isSessionCookie * True if the cookie should be a session cookie. * @returns {string} * The name of the cookie that was generated. */ function addTestCookie(isSessionCookie) { gCookieCounter++; let name = `Cookie name: ${gCookieCounter}`; Services.cookies.add( COOKIE_HOST, COOKIE_PATH, name, `Cookie value: ${gCookieCounter}`, false, false, isSessionCookie, Date.now() / 1000 + 1, COOKIE_ORIGIN_ATTRIBUTES, Ci.nsICookie.SAMESITE_NONE, Ci.nsICookie.SCHEME_HTTP ); return name; } /** * A helper function that sets up a BackupService to be instrumented to detect * a backup regeneration, and then runs an async taskFn to ensure that the * regeneration occurs. * * @param {Function} taskFn * The async function to run after the BackupService has been set up. It is * not passed any arguments. * @param {string} msg * The message to write in the assertion when the regeneration occurs. */ async function expectRegeneration(taskFn, msg) { let sandbox = sinon.createSandbox(); // Override the default REGENERATION_DEBOUNCE_RATE_MS to 0 so that we don't // have to wait long for the debouncer to fire. We need to do this before // we construct the BackupService that we'll use for the test. sandbox.stub(BackupService, "REGENERATION_DEBOUNCE_RATE_MS").get(() => 0); let bs = new BackupService(); // Now we set up some stubs on the BackupService to detect calls to // deleteLastBackup and createbackupOnIdleDispatch, which are both called // on regeneration. let deleteDeferred = Promise.withResolvers(); sandbox.stub(bs, "deleteLastBackup").callsFake(() => { Assert.ok(true, "Saw deleteLastBackup call"); deleteDeferred.resolve(); return Promise.resolve(); }); let createBackupDeferred = Promise.withResolvers(); sandbox.stub(bs, "createBackupOnIdleDispatch").callsFake(() => { Assert.ok(true, "Saw createBackupOnIdleDispatch call"); createBackupDeferred.resolve(); return Promise.resolve(); }); // Creating a new backup will only occur if scheduled backups are enabled, // so let's set the pref... Services.prefs.setBoolPref("browser.backup.scheduled.enabled", true); // But also stub out `onIdle` so that we don't get any interference during // our test by the idle service. sandbox.stub(bs, "onIdle").returns(); bs.initBackupScheduler(); await taskFn(); let regenerationPromises = [ deleteDeferred.promise, createBackupDeferred.promise, ]; // We'll wait for 1 second before considering the regeneration a bust. let timeoutPromise = new Promise((resolve, reject) => // eslint-disable-next-line mozilla/no-arbitrary-setTimeout setTimeout(() => { reject(); }, 1000) ); try { await Promise.race([Promise.all(regenerationPromises), timeoutPromise]); Assert.ok(true, msg); } catch (e) { Assert.ok(false, "Timed out waiting for regeneration."); } bs.uninitBackupScheduler(); sandbox.restore(); } /** * A helper function that sets up a BackupService to be instrumented to detect * a backup regeneration, and then runs an async taskFn to ensure that the * regeneration DOES NOT occur. * * @param {Function} taskFn * The async function to run after the BackupService has been set up. It is * not passed any arguments. * @param {string} msg * The message to write in the assertion when the regeneration does not occur * within the timeout. */ async function expectNoRegeneration(taskFn, msg) { let sandbox = sinon.createSandbox(); // Override the default REGENERATION_DEBOUNCE_RATE_MS to 0 so that we don't // have to wait long for the debouncer to fire. We need to do this before // we construct the BackupService that we'll use for the test. sandbox.stub(BackupService, "REGENERATION_DEBOUNCE_RATE_MS").get(() => 0); let bs = new BackupService(); // Now we set up some stubs on the BackupService to detect calls to // deleteLastBackup and createbackupOnIdleDispatch, which are both called // on regeneration. We share the same Promise here because in either of these // being called, this test is a failure. let regenerationPromise = Promise.withResolvers(); sandbox.stub(bs, "deleteLastBackup").callsFake(() => { Assert.ok(false, "Unexpectedly saw deleteLastBackup call"); regenerationPromise.reject(); return Promise.resolve(); }); sandbox.stub(bs, "createBackupOnIdleDispatch").callsFake(() => { Assert.ok(false, "Unexpectedly saw createBackupOnIdleDispatch call"); regenerationPromise.reject(); return Promise.resolve(); }); // Creating a new backup will only occur if scheduled backups are enabled, // so let's set the pref... Services.prefs.setBoolPref("browser.backup.scheduled.enabled", true); // But also stub out `onIdle` so that we don't get any interference during // our test by the idle service. sandbox.stub(bs, "onIdle").returns(); bs.initBackupScheduler(); await taskFn(); // We'll wait for 1 second, and if we don't see the regeneration attempt, // we'll assume it's not coming. let timeoutPromise = new Promise(resolve => // eslint-disable-next-line mozilla/no-arbitrary-setTimeout setTimeout(() => { Assert.ok(true, "Saw no regeneration within 1 second."); resolve(); }, 1000) ); try { await Promise.race([regenerationPromise.promise, timeoutPromise]); Assert.ok(true, msg); } catch (e) { Assert.ok(false, "Saw an unexpected regeneration."); } bs.uninitBackupScheduler(); sandbox.restore(); } add_setup(() => { CookieXPCShellUtils.createServer({ hosts: ["example.com"] }); Services.prefs.setBoolPref("dom.security.https_first", false); // Allow all cookies. Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); Services.prefs.setBoolPref( "network.cookieJarSettings.unblocked_for_testing", true ); }); /** * Tests that backup regeneration occurs on the page-removed PlacesObserver * event that indicates manual user deletion of a page from history. */ add_task(async function test_page_removed_reason_deleted() { const PAGE_URI = "https://test.com"; await addTestHistory(PAGE_URI); await expectRegeneration(async () => { await PlacesUtils.history.remove(PAGE_URI); }, "Saw regeneration on page-removed via user deletion."); }); /** * Tests that backup regeneration does not occur on the page-removed * PlacesObserver event that indicates automatic deletion of a page from * history. */ add_task(async function test_page_removed_reason_expired() { const PAGE_URI = "https://test.com"; await addTestHistory( PAGE_URI, 0 /* Timestamp at 0 is wayyyyy in the past, in the 1960's - it's the UNIX epoch start date */ ); await expectNoRegeneration(async () => { // This is how the Places tests force expiration, so we'll do it the same // way. let promise = TestUtils.topicObserved( PlacesUtils.TOPIC_EXPIRATION_FINISHED ); let expire = Cc["@mozilla.org/places/expiration;1"].getService( Ci.nsIObserver ); expire.observe(null, "places-debug-start-expiration", -1); await promise; }, "Saw no regeneration on page-removed via expiration."); }); /** * Tests that backup regeneration occurs on the history-cleared PlacesObserver * event that indicates clearing of all user history. */ add_task(async function test_history_cleared() { const PAGE_URI = "https://test.com"; await addTestHistory(PAGE_URI); await expectRegeneration(async () => { await PlacesUtils.history.clear(); }, "Saw regeneration on history-cleared."); }); /** * Tests that backup regeneration occurs when removing a download from history. */ add_task(async function test_download_removed() { const FAKE_FILE_PATH = PathUtils.join(PathUtils.tempDir, "somefile.zip"); let download = { source: { url: "https://test.com/somefile", isPrivate: false, }, target: { path: FAKE_FILE_PATH }, }; await DownloadHistory.addDownloadToHistory(download); await expectRegeneration(async () => { await PlacesUtils.history.remove(download.source.url); }, "Saw regeneration on download removal."); }); /** * Tests that backup regeneration occurs when clearing all downloads. */ add_task(async function test_all_downloads_removed() { const FAKE_FILE_PATH = PathUtils.join(PathUtils.tempDir, "somefile.zip"); let download1 = { source: { url: "https://test.com/somefile", isPrivate: false, }, target: { path: FAKE_FILE_PATH }, }; let download2 = { source: { url: "https://test.com/somefile2", isPrivate: false, }, target: { path: FAKE_FILE_PATH }, }; await DownloadHistory.addDownloadToHistory(download1); await DownloadHistory.addDownloadToHistory(download2); await expectRegeneration(async () => { await PlacesUtils.history.removeVisitsByFilter({ transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD, }); }, "Saw regeneration on all downloads removed."); }); /** * Tests that backup regeneration occurs when removing a password. */ add_task(async function test_password_removed() { let login = new nsLoginInfo( "https://example.com", "https://example.com", null, "notifyu1", "notifyp1", "user", "pass" ); await Services.logins.addLoginAsync(login); await expectRegeneration(async () => { Services.logins.removeLogin(login); }, "Saw regeneration on password removed."); }); /** * Tests that backup regeneration occurs when all passwords are removed. */ add_task(async function test_all_passwords_removed() { let login1 = new nsLoginInfo( "https://example.com", "https://example.com", null, "notifyu1", "notifyp1", "user", "pass" ); let login2 = new nsLoginInfo( "https://example.com", "https://example.com", null, "", "notifyp1", "", "pass" ); await Services.logins.addLoginAsync(login1); await Services.logins.addLoginAsync(login2); await expectRegeneration(async () => { Services.logins.removeAllLogins(); }, "Saw regeneration on all passwords removed."); }); /** * Tests that backup regeneration occurs when removing a bookmark. */ add_task(async function test_bookmark_removed() { let bookmark = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, url: "data:text/plain,Content", title: "Regeneration Test Bookmark", }); await expectRegeneration(async () => { await PlacesUtils.bookmarks.remove(bookmark); }, "Saw regeneration on bookmark removed."); }); /** * Tests that backup regeneration occurs when all bookmarks are removed. */ add_task(async function test_all_bookmarks_removed() { await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, url: "data:text/plain,Content", title: "Regeneration Test Bookmark 1", }); await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, url: "data:text/plain,Content2", title: "Regeneration Test Bookmark 2", }); await expectRegeneration(async () => { await PlacesUtils.bookmarks.eraseEverything(); }, "Saw regeneration on all bookmarks removed."); }); /** * Tests that backup regeneration occurs when an addon is uninstalled. */ add_task(async function test_addon_uninstalled() { await AddonTestUtils.promiseStartupManager(); let testExtension = ExtensionTestUtils.loadExtension({ manifest: { name: "Some test extension", browser_specific_settings: { gecko: { id: "test-backup-regeneration@ext-0" }, }, }, useAddonManager: "temporary", }); await testExtension.startup(); await expectRegeneration(async () => { await testExtension.unload(); }, "Saw regeneration on test extension uninstall."); }); /** * Tests that backup regeneration occurs when removing a payment method. */ add_task(async function test_payment_method_removed() { await formAutofillStorage.initialize(); let guid = await formAutofillStorage.creditCards.add({ "cc-name": "Foxy the Firefox", "cc-number": "5555555555554444", "cc-exp-month": 5, "cc-exp-year": 2099, }); await expectRegeneration(async () => { await formAutofillStorage.creditCards.remove(guid); }, "Saw regeneration on payment method removal."); }); /** * Tests that backup regeneration occurs when removing an address. */ add_task(async function test_address_removed() { await formAutofillStorage.initialize(); let guid = await formAutofillStorage.addresses.add({ "given-name": "John", "additional-name": "R.", "family-name": "Smith", organization: "World Wide Web Consortium", "street-address": "32 Vassar Street\\\nMIT Room 32-G524", "address-level2": "Cambridge", "address-level1": "MA", "postal-code": "02139", country: "US", tel: "+15195555555", email: "user@example.com", }); await expectRegeneration(async () => { await formAutofillStorage.addresses.remove(guid); }, "Saw regeneration on address removal."); }); /** * Tests that backup regeneration occurs after any kind of data sanitization. */ add_task(async function test_sanitization() { await expectRegeneration(async () => { await Sanitizer.sanitize(["cookiesAndStorage"]); }, "Saw regeneration on sanitization of cookies and storage."); await expectRegeneration(async () => { await Sanitizer.sanitize(["siteSettings"]); }, "Saw regeneration on sanitization of site settings."); }); /** * Tests that backup regeneration occurs after a permission is removed. */ add_task(async function test_permission_removed() { let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://test-permission-site.com" ); const PERMISSION_TYPE = "desktop-notification"; Services.perms.addFromPrincipal( principal, PERMISSION_TYPE, Services.perms.ALLOW_ACTION ); await expectRegeneration(async () => { Services.perms.removeFromPrincipal(principal, PERMISSION_TYPE); }, "Saw regeneration on permission removal."); }); /** * Tests that backup regeneration occurs when persistent and session cookies are * removed. */ add_task(async function test_cookies_removed() { for (let isSessionCookie of [false, true]) { Services.cookies.removeAll(); // First, let's remove a single cookie by host, path, name and origin // attrbutes. let name = addTestCookie(isSessionCookie); if (isSessionCookie) { Assert.equal( Services.cookies.sessionCookies.length, 1, "Make sure we actually added a session cookie." ); } else { Assert.equal( Services.cookies.sessionCookies.length, 0, "Make sure we actually added a persistent cookie." ); } await expectRegeneration(async () => { Services.cookies.remove( COOKIE_HOST, name, COOKIE_PATH, COOKIE_ORIGIN_ATTRIBUTES ); }, "Saw regeneration on single cookie removal."); // Now remove all cookies for a particular host. addTestCookie(isSessionCookie); addTestCookie(isSessionCookie); await expectRegeneration(async () => { Services.cookies.removeCookiesFromExactHost(COOKIE_HOST, "{}"); }, "Saw regeneration on cookie removal by host."); // Now remove all cookies. const COOKIES_TO_ADD = 10; for (let i = 0; i < COOKIES_TO_ADD; ++i) { addTestCookie(isSessionCookie); } await expectRegeneration(async () => { Services.cookies.removeAll(); }, "Saw regeneration on all cookie removal."); } }); /** * Tests that backup regeneration does not occur if a cookie is removed due * to expiry. */ add_task(async function test_cookies_not_removed_expiry() { await expectNoRegeneration(async () => { const COOKIE = "test=test"; await CookieXPCShellUtils.setCookieToDocument( "http://example.com/", COOKIE ); // Now expire that cookie by using the same value, but setting the expires // directive to sometime wayyyyy in the past. const EXPIRE = `${COOKIE}; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; await CookieXPCShellUtils.setCookieToDocument( "http://example.com/", EXPIRE ); }, "Saw no regeneration on cookie expiry."); }); /** * Tests that backup regeneration occurs when newtab links are blocked. */ add_task(async function test_newtab_link_blocked() { NewTabUtils.init(); await expectRegeneration(async () => { NewTabUtils.activityStreamLinks.blockURL("https://example.com"); }, "Saw regeneration on the blocking of a newtab link"); });