/* Any copyright is dedicated to the Public Domain. https://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { RemoteSettings } = ChromeUtils.importESModule( "resource://services-settings/remote-settings.sys.mjs" ); let bounceTrackingProtection = Cc[ "@mozilla.org/bounce-tracking-protection;1" ].getService(Ci.nsIBounceTrackingProtection); // Name of the RemoteSettings collection containing exceptions. const COLLECTION_NAME = "bounce-tracking-protection-exceptions"; // RemoteSettings collection db. let db; /** * Compare two string arrays ignoring order. * * @param {string[]} arr1 * @param {string[]} arr2 * @returns {boolean} - Whether the arrays match. */ const strArrayMatches = (arr1, arr2) => arr1.length === arr2.length && arr1.sort().every((value, index) => value === arr2.sort()[index]); add_setup(async function () { await SpecialPowers.pushPrefEnv({ set: [ [ "privacy.bounceTrackingProtection.mode", Ci.nsIBounceTrackingProtection.MODE_ENABLED, ], ["privacy.bounceTrackingProtection.requireStatefulBounces", true], ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], ], }); // Start with an empty RS collection. info(`Initializing RemoteSettings collection "${COLLECTION_NAME}".`); db = RemoteSettings(COLLECTION_NAME).db; await db.importChanges({}, Date.now(), [], { clear: true }); }); /** * Run a bounce test. * * @param {boolean} expectTrackerPurged - Whether the bounce tracker is expected * to be purged. */ async function runPurgeTest(expectTrackerPurged) { ok(!SiteDataTestUtils.hasCookies(ORIGIN_TRACKER), "No cookies initially."); await runTestBounce({ bounceType: "client", setState: "cookie-client", postBounceCallback: () => { ok( SiteDataTestUtils.hasCookies(ORIGIN_TRACKER), "Cookie added in bounce." ); }, skipSiteDataCleanup: true, expectPurge: expectTrackerPurged, }); info("After purging the site shouldn't have any data."); let hasCookies = SiteDataTestUtils.hasCookies(ORIGIN_TRACKER); if (expectTrackerPurged) { ok(!hasCookies, "Cookies purged."); } else { ok(hasCookies, "Cookies not purged."); } info("Cleanup"); bounceTrackingProtection.clearAll(); await SiteDataTestUtils.clear(); } /** * Wait until the BTP allow-list matches the expected state. * * @param {string[]} allowedSiteHosts - (Unordered) host list to match. */ async function waitForAllowListState(allowedSiteHosts) { // Ensure the site host exception list has been imported correctly. await BrowserTestUtils.waitForCondition(() => { return strArrayMatches( bounceTrackingProtection.testGetSiteHostExceptions(), allowedSiteHosts ); }, "Waiting for exceptions to be imported."); Assert.deepEqual( bounceTrackingProtection.testGetSiteHostExceptions().sort(), allowedSiteHosts.sort(), "Imported the correct site host exceptions" ); } /** * Dispatch a RemoteSettings "sync" event. * * @param {object} data - The event's data payload. * @param {object} [data.created] - Records that were created. * @param {object} [data.updated] - Records that were updated. * @param {object} [data.deleted] - Records that were removed. */ async function remoteSettingsSync({ created, updated, deleted }) { await RemoteSettings(COLLECTION_NAME).emit("sync", { data: { created, updated, deleted, }, }); } // Integration test that adds remote settings allowlist entries and checks // whether bounce trackers get purged. add_task(async function test_remote_exceptions_and_purge() { info("Run purge test without any exceptions."); await runPurgeTest(true); info("Add exceptions via RemoteSettings"); // At this point the exception list component has already initialized. Changes // made here should be picked up via the onSync listener. let entryTrackerA = await db.create({ siteHost: SITE_TRACKER }); let entryTrackerB = await db.create({ siteHost: SITE_TRACKER_B }); await db.importChanges({}, Date.now()); await remoteSettingsSync({ created: [entryTrackerA, entryTrackerB] }); await waitForAllowListState([SITE_TRACKER, SITE_TRACKER_B]); info( "Run the purge test again, this time no data should be purged because the tracker is on the exception list." ); await runPurgeTest(false); info( "Remove tracker from exception list and run purge test again. Data should be purged again." ); await db.delete(entryTrackerA.id); await db.importChanges({}, Date.now()); await remoteSettingsSync({ deleted: [entryTrackerA] }); // Ensure the site host exception list has been imported correctly. await waitForAllowListState([SITE_TRACKER_B]); await runPurgeTest(true); info("Cleanup"); await db.clear(); await db.importChanges({}, Date.now()); await remoteSettingsSync({ deleted: [entryTrackerB] }); await waitForAllowListState([]); }); // Unit test for syncing the remote settings list between BTPRemoteExceptionList // and BounceTrackingProtection. add_task(async function test_remote_exception_updates() { // Run an empty purge to ensure the list has been initialized. The remote // exception list is lazily constructed the first time a purge runs. It's // guaranteed to be initialized after the purge method resolves. await bounceTrackingProtection.testRunPurgeBounceTrackers(); info("Create foo.com, bar.com"); let entryTrackerA = await db.create({ siteHost: "foo.com" }); let entryTrackerB = await db.create({ siteHost: "bar.com" }); await db.importChanges({}, Date.now()); await remoteSettingsSync({ created: [entryTrackerA, entryTrackerB] }); await waitForAllowListState(["foo.com", "bar.com"]); info("Update foo.com -> foo2.com"); let entryTrackerAUpdated = { ...entryTrackerA }; entryTrackerAUpdated.siteHost = "foo2.com"; await db.update(entryTrackerA); await db.importChanges({}, Date.now()); await remoteSettingsSync({ updated: [{ old: entryTrackerA, new: entryTrackerAUpdated }], }); await waitForAllowListState(["foo2.com", "bar.com"]); info("Create example.com, remove foo2.com, bar.com"); let entryTrackerC = await db.create({ siteHost: "example.com" }); await db.delete(entryTrackerAUpdated.id); await db.delete(entryTrackerB.id); await db.importChanges({}, Date.now()); await remoteSettingsSync({ created: [entryTrackerC], deleted: [entryTrackerAUpdated, entryTrackerB], }); await waitForAllowListState(["example.com"]); info("Remove example.com, no hosts remain."); await db.delete(entryTrackerC.id); await db.importChanges({}, Date.now()); await remoteSettingsSync({ deleted: [entryTrackerC] }); await waitForAllowListState([]); info("Cleanup"); await db.clear(); await db.importChanges({}, Date.now()); });