summaryrefslogtreecommitdiffstats
path: root/toolkit/components/antitracking/bouncetrackingprotection/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/antitracking/bouncetrackingprotection/test
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/antitracking/bouncetrackingprotection/test')
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml20
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js73
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js121
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js89
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js63
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html59
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs19
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html11
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js275
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml7
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py133
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js307
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml8
13 files changed, 1185 insertions, 0 deletions
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml
new file mode 100644
index 0000000000..1c44d7804e
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml
@@ -0,0 +1,20 @@
+[DEFAULT]
+head = "head.js"
+prefs = [
+ "privacy.bounceTrackingProtection.enabled=true",
+ "privacy.bounceTrackingProtection.enableTestMode=true",
+ "privacy.bounceTrackingProtection.bounceTrackingPurgeTimerPeriodSec=0",
+]
+support-files = [
+ "file_start.html",
+ "file_bounce.sjs",
+ "file_bounce.html",
+]
+
+["browser_bouncetracking_oa_isolation.js"]
+
+["browser_bouncetracking_purge.js"]
+
+["browser_bouncetracking_simple.js"]
+
+["browser_bouncetracking_stateful.js"]
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js
new file mode 100644
index 0000000000..12c2c943dd
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.bounceTrackingProtection.requireStatefulBounces", true],
+ ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0],
+ ],
+ });
+});
+
+// Tests that bounces in PBM don't affect state in normal browsing.
+add_task(async function test_pbm_data_isolated() {
+ await runTestBounce({
+ bounceType: "client",
+ setState: "cookie-client",
+ originAttributes: { privateBrowsingId: 1 },
+ postBounceCallback: () => {
+ // After the PBM bounce assert that we haven't recorded any data for normal browsing.
+ Assert.equal(
+ bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length,
+ 0,
+ "No bounce tracker candidates for normal browsing."
+ );
+ Assert.equal(
+ bounceTrackingProtection.testGetUserActivationHosts({}).length,
+ 0,
+ "No user activations for normal browsing."
+ );
+ },
+ });
+});
+
+// Tests that bounces in PBM don't affect state in normal browsing.
+add_task(async function test_containers_isolated() {
+ await runTestBounce({
+ bounceType: "server",
+ setState: "cookie-server",
+ originAttributes: { userContextId: 2 },
+ postBounceCallback: () => {
+ // After the bounce in the container tab assert that we haven't recorded any data for normal browsing.
+ Assert.equal(
+ bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length,
+ 0,
+ "No bounce tracker candidates for normal browsing."
+ );
+ Assert.equal(
+ bounceTrackingProtection.testGetUserActivationHosts({}).length,
+ 0,
+ "No user activations for normal browsing."
+ );
+
+ // Or in another container tab.
+ Assert.equal(
+ bounceTrackingProtection.testGetBounceTrackerCandidateHosts({
+ userContextId: 1,
+ }).length,
+ 0,
+ "No bounce tracker candidates for container tab 1."
+ );
+ Assert.equal(
+ bounceTrackingProtection.testGetUserActivationHosts({
+ userContextId: 1,
+ }).length,
+ 0,
+ "No user activations for container tab 1."
+ );
+ },
+ });
+});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js
new file mode 100644
index 0000000000..a8e98b80f0
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BOUNCE_TRACKING_GRACE_PERIOD_SEC = 30;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec",
+ BOUNCE_TRACKING_GRACE_PERIOD_SEC,
+ ],
+ ["privacy.bounceTrackingProtection.requireStatefulBounces", false],
+ ],
+ });
+});
+
+/**
+ * The following tests ensure that sites that have open tabs are exempt from purging.
+ */
+
+function initBounceTrackerState() {
+ bounceTrackingProtection.clearAll();
+
+ // Bounce time of 1 is out of the grace period which means we should purge.
+ bounceTrackingProtection.testAddBounceTrackerCandidate({}, "example.com", 1);
+ bounceTrackingProtection.testAddBounceTrackerCandidate({}, "example.net", 1);
+
+ // Should not purge because within grace period.
+ let timestampWithinGracePeriod =
+ Date.now() - (BOUNCE_TRACKING_GRACE_PERIOD_SEC * 1000) / 2;
+ bounceTrackingProtection.testAddBounceTrackerCandidate(
+ {},
+ "example.org",
+ timestampWithinGracePeriod * 1000
+ );
+}
+
+add_task(async function test_purging_skip_open_foreground_tab() {
+ initBounceTrackerState();
+
+ // Foreground tab
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ Assert.deepEqual(
+ await bounceTrackingProtection.testRunPurgeBounceTrackers(),
+ ["example.net"],
+ "Should only purge example.net. example.org is within the grace period, example.com has an open tab."
+ );
+
+ info("Close the tab for example.com and test that it gets purged now.");
+ initBounceTrackerState();
+
+ BrowserTestUtils.removeTab(tab);
+ Assert.deepEqual(
+ (await bounceTrackingProtection.testRunPurgeBounceTrackers()).sort(),
+ ["example.net", "example.com"].sort(),
+ "example.com should have been purged now that it no longer has an open tab."
+ );
+
+ bounceTrackingProtection.clearAll();
+});
+
+add_task(async function test_purging_skip_open_background_tab() {
+ initBounceTrackerState();
+
+ // Background tab
+ let tab = BrowserTestUtils.addTab(gBrowser, "https://example.com");
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ Assert.deepEqual(
+ await bounceTrackingProtection.testRunPurgeBounceTrackers(),
+ ["example.net"],
+ "Should only purge example.net. example.org is within the grace period, example.com has an open tab."
+ );
+
+ info("Close the tab for example.com and test that it gets purged now.");
+ initBounceTrackerState();
+
+ BrowserTestUtils.removeTab(tab);
+ Assert.deepEqual(
+ (await bounceTrackingProtection.testRunPurgeBounceTrackers()).sort(),
+ ["example.net", "example.com"].sort(),
+ "example.com should have been purged now that it no longer has an open tab."
+ );
+
+ bounceTrackingProtection.clearAll();
+});
+
+add_task(async function test_purging_skip_open_tab_extra_window() {
+ initBounceTrackerState();
+
+ // Foreground tab in new window.
+ let win = await BrowserTestUtils.openNewBrowserWindow({});
+ await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ "https://example.com"
+ );
+ Assert.deepEqual(
+ await bounceTrackingProtection.testRunPurgeBounceTrackers(),
+ ["example.net"],
+ "Should only purge example.net. example.org is within the grace period, example.com has an open tab."
+ );
+
+ info(
+ "Close the window with the tab for example.com and test that it gets purged now."
+ );
+ initBounceTrackerState();
+
+ await BrowserTestUtils.closeWindow(win);
+ Assert.deepEqual(
+ (await bounceTrackingProtection.testRunPurgeBounceTrackers()).sort(),
+ ["example.net", "example.com"].sort(),
+ "example.com should have been purged now that it no longer has an open tab."
+ );
+
+ bounceTrackingProtection.clearAll();
+});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js
new file mode 100644
index 0000000000..dfbd4d0fc0
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.bounceTrackingProtection.requireStatefulBounces", false],
+ ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0],
+ ],
+ });
+});
+
+// Tests a stateless bounce via client redirect.
+add_task(async function test_client_bounce_simple() {
+ await runTestBounce({ bounceType: "client" });
+});
+
+// Tests a stateless bounce via server redirect.
+add_task(async function test_server_bounce_simple() {
+ await runTestBounce({ bounceType: "server" });
+});
+
+// Tests a chained redirect consisting of a server and a client redirect.
+add_task(async function test_bounce_chain() {
+ Assert.equal(
+ bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length,
+ 0,
+ "No bounce tracker hosts initially."
+ );
+ Assert.equal(
+ bounceTrackingProtection.testGetUserActivationHosts({}).length,
+ 0,
+ "No user activation hosts initially."
+ );
+
+ await BrowserTestUtils.withNewTab(
+ getBaseUrl(ORIGIN_A) + "file_start.html",
+ async browser => {
+ let promiseRecordBounces = waitForRecordBounces(browser);
+
+ // The final destination after the bounces.
+ let targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html");
+
+ // Construct last hop.
+ let bounceChainUrlEnd = getBounceURL({ bounceType: "server", targetURL });
+ // Construct first hop, nesting last hop.
+ let bounceChainUrlFull = getBounceURL({
+ bounceType: "client",
+ redirectDelayMS: 100,
+ bounceOrigin: ORIGIN_TRACKER_B,
+ targetURL: bounceChainUrlEnd,
+ });
+
+ info("bounceChainUrl: " + bounceChainUrlFull.href);
+
+ // Navigate through the bounce chain.
+ await navigateLinkClick(browser, bounceChainUrlFull);
+
+ // Wait for the final site to be loaded which complete the BounceTrackingRecord.
+ await BrowserTestUtils.browserLoaded(browser, false, targetURL);
+
+ // Navigate again with user gesture which triggers
+ // BounceTrackingProtection::RecordStatefulBounces. We could rely on the
+ // timeout (mClientBounceDetectionTimeout) here but that can cause races
+ // in debug where the load is quite slow.
+ await navigateLinkClick(
+ browser,
+ new URL(getBaseUrl(ORIGIN_C) + "file_start.html")
+ );
+
+ await promiseRecordBounces;
+
+ Assert.deepEqual(
+ bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).sort(),
+ [SITE_TRACKER_B, SITE_TRACKER].sort(),
+ `Identified all bounce trackers in the redirect chain.`
+ );
+ Assert.deepEqual(
+ bounceTrackingProtection.testGetUserActivationHosts({}).sort(),
+ [SITE_A, SITE_B].sort(),
+ "Should only have user activation for sites where we clicked links."
+ );
+
+ bounceTrackingProtection.clearAll();
+ }
+ );
+});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js
new file mode 100644
index 0000000000..e7fb4521a7
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let bounceTrackingProtection;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.bounceTrackingProtection.requireStatefulBounces", true],
+ ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0],
+ ],
+ });
+ bounceTrackingProtection = Cc[
+ "@mozilla.org/bounce-tracking-protection;1"
+ ].getService(Ci.nsIBounceTrackingProtection);
+});
+
+// Cookie tests.
+
+add_task(async function test_bounce_stateful_cookies_client() {
+ info("Test client bounce with cookie.");
+ await runTestBounce({
+ bounceType: "client",
+ setState: "cookie-client",
+ });
+ info("Test client bounce without cookie.");
+ await runTestBounce({
+ bounceType: "client",
+ setState: null,
+ expectCandidate: false,
+ expectPurge: false,
+ });
+});
+
+add_task(async function test_bounce_stateful_cookies_server() {
+ info("Test server bounce with cookie.");
+ await runTestBounce({
+ bounceType: "server",
+ setState: "cookie-server",
+ });
+ info("Test server bounce without cookie.");
+ await runTestBounce({
+ bounceType: "server",
+ setState: null,
+ expectCandidate: false,
+ expectPurge: false,
+ });
+});
+
+// Storage tests.
+
+// TODO: Bug 1848406: Implement stateful bounce detection for localStorage.
+add_task(async function test_bounce_stateful_localStorage() {
+ info("TODO: client bounce with localStorage.");
+ await runTestBounce({
+ bounceType: "client",
+ setState: "localStorage",
+ expectCandidate: false,
+ expectPurge: false,
+ });
+});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html
new file mode 100644
index 0000000000..2756555fa5
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <title>Bounce!</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ </head>
+ <body>
+ <p>Nothing to see here...</p>
+ <script>
+ // Wrap the entire block so we can run async code.
+ (async () => {
+ let url = new URL(location.href);
+
+ let redirectDelay = url.searchParams.get("redirectDelay");
+ if(redirectDelay != null) {
+ redirectDelay = Number.parseInt(redirectDelay);
+ } else {
+ redirectDelay = 50;
+ }
+
+ let setState = url.searchParams.get("setState");
+ if (setState) {
+ let id = Math.random().toString();
+
+ if (setState == "cookie-client") {
+ let cookie = document.cookie;
+
+ if (cookie) {
+ console.info("Received cookie", cookie);
+ } else {
+ let newCookie = `id=${id}`;
+ console.info("Setting new cookie", newCookie);
+ document.cookie = newCookie;
+ }
+ } else if (setState == "localStorage") {
+ let entry = localStorage.getItem("id");
+
+ if (entry) {
+ console.info("Found localStorage entry. id", entry);
+ } else {
+ console.info("Setting new localStorage entry. id", id);
+ localStorage.setItem(id, id);
+ }
+ }
+ }
+
+ let target = url.searchParams.get("target");
+ if (target) {
+ console.info("redirecting to", target);
+ setTimeout(() => {
+ location.href = target;
+ }, redirectDelay);
+ }
+ })();
+ </script>
+ </body>
+</html>
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs
new file mode 100644
index 0000000000..5e948a899b
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs
@@ -0,0 +1,19 @@
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ let query = new URLSearchParams(request.queryString);
+
+ let setState = query.get("setState");
+ if (setState == "cookie-server") {
+ response.setHeader("Set-Cookie", "foo=bar");
+ }
+
+ let statusCode = 302;
+ let statusCodeQuery = query.get("statusCode");
+ if (statusCodeQuery) {
+ statusCode = Number.parseInt(statusCodeQuery);
+ }
+
+ response.setStatusLine("1.1", statusCode, "Found");
+ response.setHeader("Location", query.get("target"), false);
+}
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html
new file mode 100644
index 0000000000..ded691023b
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <title>Blank</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js
new file mode 100644
index 0000000000..f5857b6919
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js
@@ -0,0 +1,275 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const SITE_A = "example.com";
+const ORIGIN_A = `https://${SITE_A}`;
+
+const SITE_B = "example.org";
+const ORIGIN_B = `https://${SITE_B}`;
+
+const SITE_C = "example.net";
+const ORIGIN_C = `https://${SITE_C}`;
+
+const SITE_TRACKER = "itisatracker.org";
+const ORIGIN_TRACKER = `https://${SITE_TRACKER}`;
+
+const SITE_TRACKER_B = "trackertest.org";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const ORIGIN_TRACKER_B = `http://${SITE_TRACKER_B}`;
+
+// Test message used for observing when the record-bounces method in
+// BounceTrackingProtection.cpp has finished.
+const OBSERVER_MSG_RECORD_BOUNCES_FINISHED = "test-record-bounces-finished";
+
+const ROOT_DIR = getRootDirectory(gTestPath);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "bounceTrackingProtection",
+ "@mozilla.org/bounce-tracking-protection;1",
+ "nsIBounceTrackingProtection"
+);
+
+/**
+ * Get the base url for the current test directory using the given origin.
+ * @param {string} origin - Origin to use in URL.
+ * @returns {string} - Generated URL as a string.
+ */
+function getBaseUrl(origin) {
+ return ROOT_DIR.replace("chrome://mochitests/content", origin);
+}
+
+/**
+ * Constructs a url for an intermediate "bounce" hop which represents a tracker.
+ * @param {*} options - URL generation options.
+ * @param {('server'|'client')} options.bounceType - Redirect type to use for
+ * the bounce.
+ * @param {string} [options.bounceOrigin] - The origin of the bounce URL.
+ * @param {string} [options.targetURL] - URL to redirect to after the bounce.
+ * @param {("cookie"|null)} [options.setState] - What type of state should be set during
+ * the bounce. No state by default.
+ * @param {number} [options.statusCode] - HTTP status code to use for server
+ * side redirect. Only applies to bounceType == "server".
+ * @param {number} [options.redirectDelayMS] - How long to wait before
+ * redirecting. Only applies to bounceType == "client".
+ * @returns {URL} Generated URL which points to an endpoint performing the
+ * redirect.
+ */
+function getBounceURL({
+ bounceType,
+ bounceOrigin = ORIGIN_TRACKER,
+ targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"),
+ setState = null,
+ statusCode = 302,
+ redirectDelayMS = 50,
+}) {
+ if (!["server", "client"].includes(bounceType)) {
+ throw new Error("Invalid bounceType");
+ }
+
+ let bounceFile =
+ bounceType == "client" ? "file_bounce.html" : "file_bounce.sjs";
+
+ let bounceUrl = new URL(getBaseUrl(bounceOrigin) + bounceFile);
+
+ let { searchParams } = bounceUrl;
+ searchParams.set("target", targetURL.href);
+ if (setState) {
+ searchParams.set("setState", setState);
+ }
+
+ if (bounceType == "server") {
+ searchParams.set("statusCode", statusCode);
+ } else if (bounceType == "client") {
+ searchParams.set("redirectDelay", redirectDelayMS);
+ }
+
+ return bounceUrl;
+}
+
+/**
+ * Insert an <a href/> element with the given target and perform a synthesized
+ * click on it.
+ * @param {MozBrowser} browser - Browser to insert the link in.
+ * @param {URL} targetURL - Destination for navigation.
+ * @returns {Promise} Resolves once the click is done. Does not wait for
+ * navigation or load.
+ */
+async function navigateLinkClick(browser, targetURL) {
+ await SpecialPowers.spawn(browser, [targetURL.href], targetURL => {
+ let link = content.document.createElement("a");
+
+ link.href = targetURL;
+ link.textContent = targetURL;
+ // The link needs display: block, otherwise synthesizeMouseAtCenter doesn't
+ // hit it.
+ link.style.display = "block";
+
+ content.document.body.appendChild(link);
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter("a[href]", {}, browser);
+}
+
+/**
+ * Wait for the record-bounces method to run for the given tab / browser.
+ * @param {browser} browser - Browser element which represents the tab we want
+ * to observe.
+ * @returns {Promise} Promise which resolves once the record-bounces method has
+ * run for the given browser.
+ */
+async function waitForRecordBounces(browser) {
+ return TestUtils.topicObserved(
+ OBSERVER_MSG_RECORD_BOUNCES_FINISHED,
+ subject => {
+ // Ensure the message was dispatched for the browser we're interested in.
+ let propBag = subject.QueryInterface(Ci.nsIPropertyBag2);
+ let browserId = propBag.getProperty("browserId");
+ return browser.browsingContext.browserId == browserId;
+ }
+ );
+}
+
+/**
+ * Test helper which loads an initial blank page, then navigates to a url which
+ * performs a bounce. Checks that the bounce hosts are properly identified as
+ * trackers.
+ * @param {object} options - Test Options.
+ * @param {('server'|'client')} options.bounceType - Whether to perform a client
+ * or server side redirect.
+ * @param {('cookie-server'|'cookie-client'|'localStorage')} [options.setState]
+ * Type of state to set during the redirect. Defaults to non stateful redirect.
+ * @param {boolean} [options.expectCandidate=true] - Expect the redirecting site to be
+ * identified as a bounce tracker (candidate).
+ * @param {boolean} [options.expectPurge=true] - Expect the redirecting site to have
+ * its storage purged.
+ * @param {OriginAttributes} [options.originAttributes={}] - Origin attributes
+ * to use for the test. This determines whether the test is run in normal
+ * browsing, a private window or a container tab. By default the test is run
+ * in normal browsing.
+ * @param {function} [options.postBounceCallback] - Optional function to run after the
+ * bounce has completed.
+ */
+async function runTestBounce(options = {}) {
+ let {
+ bounceType,
+ setState = null,
+ expectCandidate = true,
+ expectPurge = true,
+ originAttributes = {},
+ postBounceCallback = () => {},
+ } = options;
+ info(`runTestBounce ${JSON.stringify(options)}`);
+
+ Assert.equal(
+ bounceTrackingProtection.testGetBounceTrackerCandidateHosts(
+ originAttributes
+ ).length,
+ 0,
+ "No bounce tracker hosts initially."
+ );
+ Assert.equal(
+ bounceTrackingProtection.testGetUserActivationHosts(originAttributes)
+ .length,
+ 0,
+ "No user activation hosts initially."
+ );
+
+ let win = window;
+ let { privateBrowsingId, userContextId } = originAttributes;
+ let usePrivateWindow =
+ privateBrowsingId != null &&
+ privateBrowsingId !=
+ Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID;
+ if (userContextId != null && userContextId > 0 && usePrivateWindow) {
+ throw new Error("userContextId is not supported in private windows");
+ }
+
+ if (usePrivateWindow) {
+ win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ }
+
+ let tab = win.gBrowser.addTab(getBaseUrl(ORIGIN_A) + "file_start.html", {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ userContextId,
+ });
+ win.gBrowser.selectedTab = tab;
+
+ let browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let promiseRecordBounces = waitForRecordBounces(browser);
+
+ // The final destination after the bounce.
+ let targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html");
+
+ // Navigate through the bounce chain.
+ await navigateLinkClick(
+ browser,
+ getBounceURL({ bounceType, targetURL, setState })
+ );
+
+ // Wait for the final site to be loaded which complete the BounceTrackingRecord.
+ await BrowserTestUtils.browserLoaded(browser, false, targetURL);
+
+ // Navigate again with user gesture which triggers
+ // BounceTrackingProtection::RecordStatefulBounces. We could rely on the
+ // timeout (mClientBounceDetectionTimeout) here but that can cause races
+ // in debug where the load is quite slow.
+ await navigateLinkClick(
+ browser,
+ new URL(getBaseUrl(ORIGIN_C) + "file_start.html")
+ );
+
+ await promiseRecordBounces;
+
+ Assert.deepEqual(
+ bounceTrackingProtection.testGetBounceTrackerCandidateHosts(
+ originAttributes
+ ),
+ expectCandidate ? [SITE_TRACKER] : [],
+ `Should ${
+ expectCandidate ? "" : "not "
+ }have identified ${SITE_TRACKER} as a bounce tracker.`
+ );
+ Assert.deepEqual(
+ bounceTrackingProtection
+ .testGetUserActivationHosts(originAttributes)
+ .sort(),
+ [SITE_A, SITE_B].sort(),
+ "Should only have user activation for sites where we clicked links."
+ );
+
+ // If the caller specified a function to run after the bounce, run it now.
+ await postBounceCallback();
+
+ Assert.deepEqual(
+ await bounceTrackingProtection.testRunPurgeBounceTrackers(),
+ expectPurge ? [SITE_TRACKER] : [],
+ `Should ${expectPurge ? "" : "not "}purge state for ${SITE_TRACKER}.`
+ );
+
+ // Clean up
+ BrowserTestUtils.removeTab(tab);
+ if (usePrivateWindow) {
+ await BrowserTestUtils.closeWindow(win);
+
+ info(
+ "Closing the last PBM window should trigger a purge of all PBM state."
+ );
+ Assert.ok(
+ !bounceTrackingProtection.testGetBounceTrackerCandidateHosts(
+ originAttributes
+ ).length,
+ "No bounce tracker hosts after closing private window."
+ );
+ Assert.ok(
+ !bounceTrackingProtection.testGetUserActivationHosts(originAttributes)
+ .length,
+ "No user activation hosts after closing private window."
+ );
+ }
+ bounceTrackingProtection.clearAll();
+}
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml
new file mode 100644
index 0000000000..7caad6eb15
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml
@@ -0,0 +1,7 @@
+[DEFAULT]
+prefs = [
+ "privacy.bounceTrackingProtection.enabled=true",
+ "privacy.bounceTrackingProtection.enableTestMode=true",
+]
+
+["test_bouncetracking_storage_persistence.py"]
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py
new file mode 100644
index 0000000000..afc3239839
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py
@@ -0,0 +1,133 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from marionette_harness import MarionetteTestCase
+
+# Tests the persistence of the bounce tracking protection storage across
+# restarts.
+
+
+class BounceTrackingStoragePersistenceTestCase(MarionetteTestCase):
+ def setUp(self):
+ super(BounceTrackingStoragePersistenceTestCase, self).setUp()
+ self.marionette.enforce_gecko_prefs(
+ {
+ "privacy.bounceTrackingProtection.enabled": True,
+ "privacy.bounceTrackingProtection.enableTestMode": True,
+ }
+ )
+
+ self.marionette.set_context("chrome")
+ self.populate_state()
+
+ def populate_state(self):
+ # Add some data to test persistence.
+ self.marionette.execute_script(
+ """
+ let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+
+ bounceTrackingProtection.testAddBounceTrackerCandidate({}, "bouncetracker.net", Date.now() * 10000);
+ bounceTrackingProtection.testAddBounceTrackerCandidate({}, "bouncetracker.org", Date.now() * 10000);
+ bounceTrackingProtection.testAddBounceTrackerCandidate({ userContextId: 3 }, "tracker.com", Date.now() * 10000);
+ // A private browsing entry which must not be persisted across restarts.
+ bounceTrackingProtection.testAddBounceTrackerCandidate({ privateBrowsingId: 1 }, "tracker.net", Date.now() * 10000);
+
+ bounceTrackingProtection.testAddUserActivation({}, "example.com", (Date.now() + 5000) * 10000);
+ // A private browsing entry which must not be persisted across restarts.
+ bounceTrackingProtection.testAddUserActivation({ privateBrowsingId: 1 }, "example.org", (Date.now() + 2000) * 10000);
+ """
+ )
+
+ def test_state_after_restart(self):
+ self.marionette.restart(clean=False, in_app=True)
+ bounceTrackerCandidates = self.marionette.execute_script(
+ """
+ let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+ return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).sort();
+ """,
+ )
+ self.assertEqual(
+ len(bounceTrackerCandidates),
+ 2,
+ msg="There should be two entries for default OA",
+ )
+ self.assertEqual(bounceTrackerCandidates[0], "bouncetracker.net")
+ self.assertEqual(bounceTrackerCandidates[1], "bouncetracker.org")
+
+ bounceTrackerCandidates = self.marionette.execute_script(
+ """
+ let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+ return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ userContextId: 3 }).sort();
+ """,
+ )
+ self.assertEqual(
+ len(bounceTrackerCandidates),
+ 1,
+ msg="There should be only one entry for user context 3",
+ )
+ self.assertEqual(bounceTrackerCandidates[0], "tracker.com")
+
+ # Unrelated user context should not have any entries.
+ bounceTrackerCandidates = self.marionette.execute_script(
+ """
+ let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+ return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ userContextId: 4 }).length;
+ """,
+ )
+ self.assertEqual(
+ bounceTrackerCandidates,
+ 0,
+ msg="There should be no entries for user context 4",
+ )
+
+ # Private browsing entries should not be persisted across restarts.
+ bounceTrackerCandidates = self.marionette.execute_script(
+ """
+ let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+ return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ privateBrowsingId: 1 }).length;
+ """,
+ )
+ self.assertEqual(
+ bounceTrackerCandidates,
+ 0,
+ msg="There should be no entries for private browsing",
+ )
+
+ userActivations = self.marionette.execute_script(
+ """
+ let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+ return bounceTrackingProtection.testGetUserActivationHosts({}).sort();
+ """,
+ )
+ self.assertEqual(
+ len(userActivations),
+ 1,
+ msg="There should be only one entry for user activation",
+ )
+ self.assertEqual(userActivations[0], "example.com")
+
+ # Private browsing entries should not be persisted across restarts.
+ userActivations = self.marionette.execute_script(
+ """
+ let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+ return bounceTrackingProtection.testGetUserActivationHosts({ privateBrowsingId: 1 }).length;
+ """,
+ )
+ self.assertEqual(
+ userActivations, 0, msg="There should be no entries for private browsing"
+ )
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js
new file mode 100644
index 0000000000..5ede57a08b
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js
@@ -0,0 +1,307 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+
+let btp;
+let bounceTrackingGracePeriodSec;
+let bounceTrackingActivationLifetimeSec;
+
+/**
+ * Adds brackets to a host if it's an IPv6 address.
+ * @param {string} host - Host which may be an IPv6.
+ * @returns {string} bracketed IPv6 or host if host is not an IPv6.
+ */
+function maybeFixupIpv6(host) {
+ if (!host.includes(":")) {
+ return host;
+ }
+ return `[${host}]`;
+}
+
+/**
+ * Adds cookies and indexedDB test data for the given host.
+ * @param {string} host
+ */
+async function addStateForHost(host) {
+ info(`adding state for host ${host}`);
+ SiteDataTestUtils.addToCookies({ host });
+ await SiteDataTestUtils.addToIndexedDB(`https://${maybeFixupIpv6(host)}`);
+}
+
+/**
+ * Checks if the given host as cookies or indexedDB data.
+ * @param {string} host
+ * @returns {boolean}
+ */
+async function hasStateForHost(host) {
+ let origin = `https://${maybeFixupIpv6(host)}`;
+ if (SiteDataTestUtils.hasCookies(origin)) {
+ return true;
+ }
+ return SiteDataTestUtils.hasIndexedDB(origin);
+}
+
+/**
+ * Assert that there are no bounce tracker candidates or user activations
+ * recorded.
+ */
+function assertEmpty() {
+ Assert.equal(
+ btp.testGetBounceTrackerCandidateHosts({}).length,
+ 0,
+ "No tracker candidates."
+ );
+ Assert.equal(
+ btp.testGetUserActivationHosts({}).length,
+ 0,
+ "No user activation hosts."
+ );
+}
+
+add_setup(function () {
+ // Need a profile to data clearing calls.
+ do_get_profile();
+
+ btp = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+
+ // Reset global bounce tracking state.
+ btp.clearAll();
+
+ bounceTrackingGracePeriodSec = Services.prefs.getIntPref(
+ "privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec"
+ );
+ bounceTrackingActivationLifetimeSec = Services.prefs.getIntPref(
+ "privacy.bounceTrackingProtection.bounceTrackingActivationLifetimeSec"
+ );
+});
+
+/**
+ * When both maps are empty running PurgeBounceTrackers should be a no-op.
+ */
+add_task(async function test_empty() {
+ assertEmpty();
+
+ info("Run PurgeBounceTrackers");
+ await btp.testRunPurgeBounceTrackers();
+
+ assertEmpty();
+});
+
+/**
+ * Tests that the PurgeBounceTrackers behaves as expected by adding site state
+ * and adding simulated bounce state and user activations.
+ */
+add_task(async function test_purge() {
+ let now = Date.now();
+
+ // Epoch in MS.
+ let timestampWithinGracePeriod =
+ now - (bounceTrackingGracePeriodSec * 1000) / 2;
+ let timestampWithinGracePeriod2 =
+ now - (bounceTrackingGracePeriodSec * 1000) / 4;
+ let timestampOutsideGracePeriodFiveSeconds =
+ now - (bounceTrackingGracePeriodSec + 5) * 1000;
+ let timestampOutsideGracePeriodThreeDays =
+ now - (bounceTrackingGracePeriodSec + 60 * 60 * 24 * 3) * 1000;
+ let timestampFuture = now + bounceTrackingGracePeriodSec * 1000 * 2;
+
+ let timestampValidUserActivation =
+ now - (bounceTrackingActivationLifetimeSec * 1000) / 2;
+ let timestampExpiredUserActivationFourSeconds =
+ now - (bounceTrackingActivationLifetimeSec + 4) * 1000;
+ let timestampExpiredUserActivationTenDays =
+ now - (bounceTrackingActivationLifetimeSec + 60 * 60 * 24 * 10) * 1000;
+
+ const TEST_TRACKERS = {
+ "example.com": {
+ bounceTime: timestampWithinGracePeriod,
+ userActivationTime: null,
+ message: "Should not purge within grace period.",
+ shouldPurge: bounceTrackingGracePeriodSec == 0,
+ },
+ "example2.com": {
+ bounceTime: timestampWithinGracePeriod2,
+ userActivationTime: null,
+ message: "Should not purge within grace period (2).",
+ shouldPurge: bounceTrackingGracePeriodSec == 0,
+ },
+ "example.net": {
+ bounceTime: timestampOutsideGracePeriodFiveSeconds,
+ userActivationTime: null,
+ message: "Should purge after grace period.",
+ shouldPurge: true,
+ },
+ // Also ensure that clear data calls with IP sites succeed.
+ "1.2.3.4": {
+ bounceTime: timestampOutsideGracePeriodThreeDays,
+ userActivationTime: null,
+ message: "Should purge after grace period (2).",
+ shouldPurge: true,
+ },
+ "2606:4700:4700::1111": {
+ bounceTime: timestampOutsideGracePeriodThreeDays,
+ userActivationTime: null,
+ message: "Should purge after grace period (3).",
+ shouldPurge: true,
+ },
+ "example.org": {
+ bounceTime: timestampWithinGracePeriod,
+ userActivationTime: null,
+ message: "Should not purge within grace period.",
+ shouldPurge: false,
+ },
+ "example2.org": {
+ bounceTime: timestampFuture,
+ userActivationTime: null,
+ message: "Should not purge for future bounce time (within grace period).",
+ shouldPurge: false,
+ },
+ "1.1.1.1": {
+ bounceTime: null,
+ userActivationTime: timestampValidUserActivation,
+ message: "Should not purge without bounce (valid user activation).",
+ shouldPurge: false,
+ },
+ // Also testing domains with trailing ".".
+ "mozilla.org.": {
+ bounceTime: null,
+ userActivationTime: timestampExpiredUserActivationFourSeconds,
+ message: "Should not purge without bounce (expired user activation).",
+ shouldPurge: false,
+ },
+ "firefox.com": {
+ bounceTime: null,
+ userActivationTime: timestampExpiredUserActivationTenDays,
+ message: "Should not purge without bounce (expired user activation) (2).",
+ shouldPurge: false,
+ },
+ };
+
+ info("Assert empty initially.");
+ assertEmpty();
+
+ info("Populate bounce and user activation sets.");
+
+ let expectedBounceTrackerHosts = [];
+ let expectedUserActivationHosts = [];
+
+ let expiredUserActivationHosts = [];
+ let expectedPurgedHosts = [];
+
+ // This would normally happen over time while browsing.
+ let initPromises = Object.entries(TEST_TRACKERS).map(
+ async ([siteHost, { bounceTime, userActivationTime, shouldPurge }]) => {
+ // Add site state so we can later assert it has been purged.
+ await addStateForHost(siteHost);
+
+ if (bounceTime != null) {
+ if (userActivationTime != null) {
+ throw new Error(
+ "Attempting to construct invalid map state. testGetBounceTrackerCandidateHosts({}) and testGetUserActivationHosts({}) must be disjoint."
+ );
+ }
+
+ expectedBounceTrackerHosts.push(siteHost);
+
+ // Convert bounceTime timestamp to nanoseconds (PRTime).
+ info(
+ `Adding bounce. siteHost: ${siteHost}, bounceTime: ${bounceTime} ms`
+ );
+ btp.testAddBounceTrackerCandidate({}, siteHost, bounceTime * 1000);
+ }
+
+ if (userActivationTime != null) {
+ if (bounceTime != null) {
+ throw new Error(
+ "Attempting to construct invalid map state. testGetBounceTrackerCandidateHosts({}) and testGetUserActivationHosts({}) must be disjoint."
+ );
+ }
+
+ expectedUserActivationHosts.push(siteHost);
+ if (
+ userActivationTime + bounceTrackingActivationLifetimeSec * 1000 >
+ now
+ ) {
+ expiredUserActivationHosts.push(siteHost);
+ }
+
+ // Convert userActivationTime timestamp to nanoseconds (PRTime).
+ info(
+ `Adding user interaction. siteHost: ${siteHost}, userActivationTime: ${userActivationTime} ms`
+ );
+ btp.testAddUserActivation({}, siteHost, userActivationTime * 1000);
+ }
+
+ if (shouldPurge) {
+ expectedPurgedHosts.push(siteHost);
+ }
+ }
+ );
+ await Promise.all(initPromises);
+
+ info(
+ "Check that bounce and user activation data has been correctly recorded."
+ );
+ Assert.deepEqual(
+ btp.testGetBounceTrackerCandidateHosts({}).sort(),
+ expectedBounceTrackerHosts.sort(),
+ "Has added bounce tracker hosts."
+ );
+ Assert.deepEqual(
+ btp.testGetUserActivationHosts({}).sort(),
+ expectedUserActivationHosts.sort(),
+ "Has added user activation hosts."
+ );
+
+ info("Run PurgeBounceTrackers");
+ let actualPurgedHosts = await btp.testRunPurgeBounceTrackers();
+
+ Assert.deepEqual(
+ actualPurgedHosts.sort(),
+ expectedPurgedHosts.sort(),
+ "Should have purged all expected hosts."
+ );
+
+ let expectedBounceTrackerHostsAfterPurge = expectedBounceTrackerHosts
+ .filter(host => !expectedPurgedHosts.includes(host))
+ .sort();
+ Assert.deepEqual(
+ btp.testGetBounceTrackerCandidateHosts({}).sort(),
+ expectedBounceTrackerHostsAfterPurge.sort(),
+ "After purge the bounce tracker candidate host set should be updated correctly."
+ );
+
+ Assert.deepEqual(
+ btp.testGetUserActivationHosts({}).sort(),
+ expiredUserActivationHosts.sort(),
+ "After purge any expired user activation records should have been removed"
+ );
+
+ info("Test that we actually purged the correct sites.");
+ for (let siteHost of expectedPurgedHosts) {
+ Assert.ok(
+ !(await hasStateForHost(siteHost)),
+ `Site ${siteHost} should no longer have state.`
+ );
+ }
+ for (let siteHost of expectedBounceTrackerHostsAfterPurge) {
+ Assert.ok(
+ await hasStateForHost(siteHost),
+ `Site ${siteHost} should still have state.`
+ );
+ }
+
+ info("Reset bounce tracking state.");
+ btp.clearAll();
+ assertEmpty();
+
+ info("Clean up site data.");
+ await SiteDataTestUtils.clear();
+});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..16e270b85c
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml
@@ -0,0 +1,8 @@
+[DEFAULT]
+prefs = [
+ "privacy.bounceTrackingProtection.enabled=true",
+ "privacy.bounceTrackingProtection.enableTestMode=true",
+ "privacy.bounceTrackingProtection.bounceTrackingPurgeTimerPeriodSec=0",
+]
+
+["test_bouncetracking_purge.js"]