summaryrefslogtreecommitdiffstats
path: root/toolkit/components/antitracking/test/xpcshell
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/antitracking/test/xpcshell')
-rw-r--r--toolkit/components/antitracking/test/xpcshell/data/font.woffbin0 -> 1112 bytes
-rw-r--r--toolkit/components/antitracking/test/xpcshell/head.js11
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js107
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js94
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js223
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js728
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js175
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_staticPartition_authhttp.js125
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_staticPartition_clientAuthRemember.js129
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_staticPartition_font.js112
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js86
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js178
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js187
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js538
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_validate_strip_on_share_list.js100
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_view_source.js78
-rw-r--r--toolkit/components/antitracking/test/xpcshell/xpcshell.toml52
17 files changed, 2923 insertions, 0 deletions
diff --git a/toolkit/components/antitracking/test/xpcshell/data/font.woff b/toolkit/components/antitracking/test/xpcshell/data/font.woff
new file mode 100644
index 0000000000..acda4f3d9f
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/data/font.woff
Binary files differ
diff --git a/toolkit/components/antitracking/test/xpcshell/head.js b/toolkit/components/antitracking/test/xpcshell/head.js
new file mode 100644
index 0000000000..f9bf797641
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/head.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from ../../../../components/url-classifier/tests/unit/head_urlclassifier.js */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
diff --git a/toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js b/toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js
new file mode 100644
index 0000000000..4063d067f5
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js
@@ -0,0 +1,107 @@
+// This test ensures that the URL decoration annotations service works as
+// expected, and also we successfully downgrade document.referrer to the
+// eTLD+1 URL when tracking identifiers controlled by this service are
+// present in the referrer URI.
+
+"use strict";
+
+/* Unit tests for the nsIPartitioningExceptionListService implementation. */
+
+const { RemoteSettings } = ChromeUtils.importESModule(
+ "resource://services-settings/remote-settings.sys.mjs"
+);
+
+const COLLECTION_NAME = "partitioning-exempt-urls";
+const PREF_NAME = "privacy.restrict3rdpartystorage.skip_list";
+
+do_get_profile();
+
+class UpdateEvent extends EventTarget {}
+function waitForEvent(element, eventName) {
+ return new Promise(function (resolve) {
+ element.addEventListener(eventName, e => resolve(e.detail), { once: true });
+ });
+}
+
+add_task(async _ => {
+ let peuService = Cc[
+ "@mozilla.org/partitioning/exception-list-service;1"
+ ].getService(Ci.nsIPartitioningExceptionListService);
+
+ // Make sure we have a pref initially, since the exception list service
+ // requires it.
+ Services.prefs.setStringPref(PREF_NAME, "");
+
+ let updateEvent = new UpdateEvent();
+ let records = [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ firstPartyOrigin: "https://example.org",
+ thirdPartyOrigin: "https://tracking.example.com",
+ },
+ ];
+
+ // Add some initial data
+ let db = RemoteSettings(COLLECTION_NAME).db;
+ await db.importChanges({}, Date.now(), records);
+
+ let promise = waitForEvent(updateEvent, "update");
+ let obs = data => {
+ let event = new CustomEvent("update", { detail: data });
+ updateEvent.dispatchEvent(event);
+ };
+ peuService.registerAndRunExceptionListObserver(obs);
+ let list = await promise;
+ Assert.equal(list, "", "No items in the list");
+
+ // Second event is from the RemoteSettings record.
+ list = await waitForEvent(updateEvent, "update");
+ Assert.equal(
+ list,
+ "https://example.org,https://tracking.example.com",
+ "Has one item in the list"
+ );
+
+ records.push({
+ id: "2",
+ last_modified: 1000000000000002,
+ firstPartyOrigin: "https://foo.org",
+ thirdPartyOrigin: "https://bar.com",
+ });
+
+ promise = waitForEvent(updateEvent, "update");
+ await RemoteSettings(COLLECTION_NAME).emit("sync", {
+ data: { current: records },
+ });
+ list = await promise;
+ Assert.equal(
+ list,
+ "https://example.org,https://tracking.example.com;https://foo.org,https://bar.com",
+ "Has several items in the list"
+ );
+
+ promise = waitForEvent(updateEvent, "update");
+ Services.prefs.setStringPref(PREF_NAME, "https://test.com,https://test3.com");
+ list = await promise;
+ Assert.equal(
+ list,
+ "https://test.com,https://test3.com;https://example.org,https://tracking.example.com;https://foo.org,https://bar.com",
+ "Has several items in the list"
+ );
+
+ promise = waitForEvent(updateEvent, "update");
+ Services.prefs.setStringPref(
+ PREF_NAME,
+ "https://test.com,https://test3.com;https://abc.com,https://def.com"
+ );
+ list = await promise;
+ Assert.equal(
+ list,
+ "https://test.com,https://test3.com;https://abc.com,https://def.com;https://example.org,https://tracking.example.com;https://foo.org,https://bar.com",
+ "Has several items in the list"
+ );
+
+ peuService.unregisterExceptionListObserver(obs);
+ await db.clear();
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js b/toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js
new file mode 100644
index 0000000000..3ce1f8bfb7
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js
@@ -0,0 +1,94 @@
+/* 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/. */
+
+// Note: This test may cause intermittents if run at exactly midnight.
+
+"use strict";
+
+const PREF_FPI = "privacy.firstparty.isolate";
+const PREF_COOKIE_BEHAVIOR = "network.cookie.cookieBehavior";
+const PREF_COOKIE_BEHAVIOR_PBMODE = "network.cookie.cookieBehavior.pbmode";
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(PREF_FPI);
+ Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR);
+ Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR_PBMODE);
+});
+
+add_task(function test_FPI_off() {
+ Services.prefs.setBoolPref(PREF_FPI, false);
+
+ for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) {
+ Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, i);
+ equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR), i);
+ equal(Services.cookies.getCookieBehavior(false), i);
+ }
+
+ Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR);
+
+ for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) {
+ Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR_PBMODE, i);
+ equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR_PBMODE), i);
+ equal(Services.cookies.getCookieBehavior(true), i);
+ }
+});
+
+add_task(function test_FPI_on() {
+ Services.prefs.setBoolPref(PREF_FPI, true);
+
+ for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) {
+ Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, i);
+ equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR), i);
+ equal(
+ Services.cookies.getCookieBehavior(false),
+ i == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ ? Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ : i
+ );
+ }
+
+ Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR);
+
+ for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) {
+ Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR_PBMODE, i);
+ equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR_PBMODE), i);
+ equal(
+ Services.cookies.getCookieBehavior(true),
+ i == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ ? Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ : i
+ );
+ }
+
+ Services.prefs.clearUserPref(PREF_FPI);
+});
+
+add_task(function test_private_cookieBehavior_mirroring() {
+ // Test that the private cookieBehavior getter will return the regular pref if
+ // the regular pref has a user value and the private pref has a default value.
+ Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR_PBMODE);
+ for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) {
+ Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, i);
+ if (!Services.prefs.prefHasUserValue(PREF_COOKIE_BEHAVIOR)) {
+ continue;
+ }
+
+ equal(Services.cookies.getCookieBehavior(true), i);
+ }
+
+ // Test that the private cookieBehavior getter will always return the private
+ // pref if the private cookieBehavior has a user value.
+ for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) {
+ Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR_PBMODE, i);
+ if (!Services.prefs.prefHasUserValue(PREF_COOKIE_BEHAVIOR_PBMODE)) {
+ continue;
+ }
+
+ for (let j = 0; j <= Ci.nsICookieService.BEHAVIOR_LAST; ++j) {
+ Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, j);
+
+ equal(Services.cookies.getCookieBehavior(true), i);
+ }
+ }
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js b/toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js
new file mode 100644
index 0000000000..cdf2cdec2c
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js
@@ -0,0 +1,223 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+CookieXPCShellUtils.init(this);
+
+const TEST_CASES = [
+ // Tests for different schemes.
+ {
+ url: "http://example.com/",
+ partitionKeySite: "(http,example.com)",
+ partitionKeyWithoutSite: "example.com",
+ },
+ {
+ url: "https://example.com/",
+ partitionKeySite: "(https,example.com)",
+ partitionKeyWithoutSite: "example.com",
+ },
+ // Tests for sub domains
+ {
+ url: "http://sub.example.com/",
+ partitionKeySite: "(http,example.com)",
+ partitionKeyWithoutSite: "example.com",
+ },
+ {
+ url: "http://sub.sub.example.com/",
+ partitionKeySite: "(http,example.com)",
+ partitionKeyWithoutSite: "example.com",
+ },
+ // Tests for path and query.
+ {
+ url: "http://www.example.com/path/to/somewhere/",
+ partitionKeySite: "(http,example.com)",
+ partitionKeyWithoutSite: "example.com",
+ },
+ {
+ url: "http://www.example.com/?query=string",
+ partitionKeySite: "(http,example.com)",
+ partitionKeyWithoutSite: "example.com",
+ },
+ // Tests for other ports.
+ {
+ url: "http://example.com:8080/",
+ partitionKeySite: "(http,example.com)",
+ partitionKeyWithoutSite: "example.com",
+ },
+ {
+ url: "https://example.com:8080/",
+ partitionKeySite: "(https,example.com)",
+ partitionKeyWithoutSite: "example.com",
+ },
+ // Tests for about urls
+ {
+ url: "about:about",
+ partitionKeySite:
+ "(about,about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla)",
+ partitionKeyWithoutSite:
+ "about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla",
+ },
+ {
+ url: "about:preferences",
+ partitionKeySite:
+ "(about,about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla)",
+ partitionKeyWithoutSite:
+ "about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla",
+ },
+ // Test for ip addresses
+ {
+ url: "http://127.0.0.1/",
+ partitionKeySite: "(http,127.0.0.1)",
+ partitionKeyWithoutSite: "127.0.0.1",
+ },
+ {
+ url: "http://127.0.0.1:8080/",
+ partitionKeySite: "(http,127.0.0.1,8080)",
+ partitionKeyWithoutSite: "127.0.0.1",
+ },
+ {
+ url: "http://[2001:db8::ff00:42:8329]",
+ partitionKeySite: "(http,[2001:db8::ff00:42:8329])",
+ partitionKeyWithoutSite: "[2001:db8::ff00:42:8329]",
+ },
+ {
+ url: "http://[2001:db8::ff00:42:8329]:8080",
+ partitionKeySite: "(http,[2001:db8::ff00:42:8329],8080)",
+ partitionKeyWithoutSite: "[2001:db8::ff00:42:8329]",
+ },
+ // Tests for moz-extension
+ {
+ url: "moz-extension://bafa4a3f-5c49-48d6-9788-03489419b70e",
+ partitionKeySite: "",
+ partitionKeyWithoutSite: "",
+ },
+ // Tests for non tld
+ {
+ url: "http://notld",
+ partitionKeySite: "(http,notld)",
+ partitionKeyWithoutSite: "notld",
+ },
+ {
+ url: "http://com",
+ partitionKeySite: "(http,com)",
+ partitionKeyWithoutSite: "com",
+ },
+ {
+ url: "http://com:8080",
+ partitionKeySite: "(http,com,8080)",
+ partitionKeyWithoutSite: "com",
+ },
+];
+
+const TEST_INVALID_URLS = [
+ "",
+ "/foo",
+ "An invalid URL",
+ "https://",
+ "http:///",
+ "http://foo:bar",
+];
+
+add_task(async function test_get_partition_key_from_url() {
+ for (const test of TEST_CASES) {
+ info(`Testing url: ${test.url}`);
+ let partitionKey = ChromeUtils.getPartitionKeyFromURL(test.url);
+
+ Assert.equal(
+ partitionKey,
+ test.partitionKeySite,
+ "The partitionKey is correct."
+ );
+ }
+});
+
+add_task(async function test_get_partition_key_from_url_without_site() {
+ Services.prefs.setBoolPref("privacy.dynamic_firstparty.use_site", false);
+
+ for (const test of TEST_CASES) {
+ info(`Testing url: ${test.url}`);
+ let partitionKey = ChromeUtils.getPartitionKeyFromURL(test.url);
+
+ Assert.equal(
+ partitionKey,
+ test.partitionKeyWithoutSite,
+ "The partitionKey is correct."
+ );
+ }
+
+ Services.prefs.clearUserPref("privacy.dynamic_firstparty.use_site");
+});
+
+add_task(async function test_blob_url() {
+ do_get_profile();
+
+ const server = CookieXPCShellUtils.createServer({
+ hosts: ["example.org", "foo.com"],
+ });
+
+ server.registerPathHandler("/empty", (metadata, response) => {
+ var body = "<h1>Hello!</h1>";
+ response.write(body);
+ });
+
+ server.registerPathHandler("/iframe", (metadata, response) => {
+ var body = `
+ <script>
+ var blobUrl = URL.createObjectURL(new Blob([]));
+ parent.postMessage(blobUrl, "http://example.org");
+ </script>
+ `;
+ response.write(body);
+ });
+
+ let contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/empty"
+ );
+
+ let blobUrl = await contentPage.spawn([], async () => {
+ // Create a third-party iframe and create a blob url in there.
+ let f = this.content.document.createElement("iframe");
+ f.src = "http://foo.com/iframe";
+
+ let blob_url = await new Promise(resolve => {
+ this.content.addEventListener("message", event => resolve(event.data), {
+ once: true,
+ });
+ this.content.document.body.append(f);
+ });
+
+ return blob_url;
+ });
+
+ let partitionKey = ChromeUtils.getPartitionKeyFromURL(blobUrl);
+
+ // The partitionKey of the blob url is empty because the principal of the
+ // blob url is the JS principal of the global, which doesn't have
+ // partitionKey. And ChromeUtils.getPartitionKeyFromURL() will get
+ // partitionKey from that principal. So, we will get an empty partitionKey
+ // here.
+ // XXX: The behavior here is debatable.
+ Assert.equal(partitionKey, "", "The partitionKey of blob url is correct.");
+
+ await contentPage.close();
+});
+
+add_task(async function test_throw_with_invalid_URL() {
+ // The API should throw if the url is invalid.
+ for (const invalidURL of TEST_INVALID_URLS) {
+ info(`Testing invalid url: ${invalidURL}`);
+
+ Assert.throws(
+ () => {
+ ChromeUtils.getPartitionKeyFromURL(invalidURL);
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "It should fail on invalid URLs."
+ );
+ }
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js
new file mode 100644
index 0000000000..668e905b6c
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js
@@ -0,0 +1,728 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TRACKING_PAGE = "https://tracking.example.org";
+const HTTP_TRACKING_PAGE = "http://tracking.example.org";
+const TRACKING_PAGE2 =
+ "https://tracking.example.org^partitionKey=(https,example.com)";
+const HTTP_TRACKING_PAGE2 =
+ "http://tracking.example.org^partitionKey=(https,example.com)";
+const BENIGN_PAGE = "https://example.com";
+const FOREIGN_PAGE = "https://example.net";
+const FOREIGN_PAGE2 = "https://example.net^partitionKey=(https,example.com)";
+const FOREIGN_PAGE3 = "https://example.net^partitionKey=(https,example.org)";
+
+const { UrlClassifierTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "PurgeTrackerService",
+ "@mozilla.org/purge-tracker-service;1",
+ "nsIPurgeTrackerService"
+);
+
+async function setupTest(aCookieBehavior) {
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", aCookieBehavior);
+ Services.prefs.setBoolPref("privacy.purge_trackers.enabled", true);
+ Services.prefs.setCharPref("privacy.purge_trackers.logging.level", "Debug");
+ Services.prefs.setStringPref(
+ "urlclassifier.trackingAnnotationTable.testEntries",
+ "tracking.example.org"
+ );
+
+ // Enables us to test localStorage in xpcshell.
+ Services.prefs.setBoolPref("dom.storage.client_validation", false);
+}
+
+/**
+ * Test that purging doesn't happen when it shouldn't happen.
+ */
+add_task(async function testNotPurging() {
+ await UrlClassifierTestUtils.addTestTrackers();
+ setupTest(Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN);
+ SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE });
+
+ Services.prefs.setIntPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_ACCEPT
+ );
+ await PurgeTrackerService.purgeTrackingCookieJars();
+ ok(SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie remains.");
+ Services.prefs.setIntPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN
+ );
+
+ Services.prefs.setBoolPref("privacy.purge_trackers.enabled", false);
+ await PurgeTrackerService.purgeTrackingCookieJars();
+ ok(SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie remains.");
+ Services.prefs.setBoolPref("privacy.purge_trackers.enabled", true);
+
+ Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.history", true);
+ await PurgeTrackerService.purgeTrackingCookieJars();
+ ok(SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie remains.");
+ Services.prefs.clearUserPref("privacy.sanitize.sanitizeOnShutdown");
+ Services.prefs.clearUserPref("privacy.clearOnShutdown.history");
+
+ await PurgeTrackerService.purgeTrackingCookieJars();
+ ok(!SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie cleared.");
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+/**
+ * Test that cookies indexedDB and localStorage are purged if the cookie is found
+ * on the tracking list and does not have an Interaction Permission.
+ */
+async function testIndexedDBAndLocalStorage() {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ PermissionTestUtils.add(
+ TRACKING_PAGE,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ SiteDataTestUtils.addToCookies({ origin: BENIGN_PAGE });
+ for (let url of [
+ TRACKING_PAGE,
+ TRACKING_PAGE2,
+ FOREIGN_PAGE,
+ FOREIGN_PAGE2,
+ FOREIGN_PAGE3,
+ ]) {
+ SiteDataTestUtils.addToLocalStorage(url);
+ SiteDataTestUtils.addToCookies({ origin: url });
+ await SiteDataTestUtils.addToIndexedDB(url);
+ }
+
+ // Purge while storage access permission exists.
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) {
+ ok(
+ SiteDataTestUtils.hasCookies(url),
+ "cookie remains while storage access permission exists."
+ );
+ ok(
+ SiteDataTestUtils.hasLocalStorage(url),
+ "localStorage should not have been removed while storage access permission exists."
+ );
+ Assert.greater(
+ await SiteDataTestUtils.getQuotaUsage(url),
+ 0,
+ `We have data for ${url}`
+ );
+ }
+
+ // Run purge after storage access permission has been removed.
+ PermissionTestUtils.remove(TRACKING_PAGE, "storageAccessAPI");
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ ok(
+ SiteDataTestUtils.hasCookies(BENIGN_PAGE),
+ "A non-tracking page should retain cookies after purging"
+ );
+
+ for (let url of [FOREIGN_PAGE, FOREIGN_PAGE2, FOREIGN_PAGE3]) {
+ ok(
+ SiteDataTestUtils.hasCookies(url),
+ `A non-tracking foreign page should retain cookies after purging`
+ );
+ ok(
+ SiteDataTestUtils.hasLocalStorage(url),
+ `localStorage for ${url} should not have been removed.`
+ );
+ Assert.greater(
+ await SiteDataTestUtils.getQuotaUsage(url),
+ 0,
+ `We have data for ${url}`
+ );
+ }
+
+ // Cookie should have been removed.
+
+ for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) {
+ ok(
+ !SiteDataTestUtils.hasCookies(url),
+ "cookie is removed after purge with no storage access permission."
+ );
+ ok(
+ !SiteDataTestUtils.hasLocalStorage(url),
+ "localStorage should have been removed"
+ );
+ Assert.equal(
+ await SiteDataTestUtils.getQuotaUsage(url),
+ 0,
+ "quota storage was deleted"
+ );
+ }
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+}
+
+/**
+ * Test that trackers are treated based on their base domain, not origin.
+ */
+async function testBaseDomain() {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ let associatedOrigins = [
+ "https://itisatracker.org",
+ "https://sub.itisatracker.org",
+ "https://www.itisatracker.org",
+ "https://sub.sub.sub.itisatracker.org",
+ "http://itisatracker.org",
+ "http://sub.itisatracker.org",
+ ];
+
+ for (let permissionOrigin of associatedOrigins) {
+ // Only one of the associated origins gets permission, but
+ // all should be exempt from purging.
+ PermissionTestUtils.add(
+ permissionOrigin,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ for (let origin of associatedOrigins) {
+ SiteDataTestUtils.addToCookies({ origin });
+ }
+
+ // Add another tracker to verify we're actually purging.
+ SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE });
+
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ for (let origin of associatedOrigins) {
+ ok(
+ SiteDataTestUtils.hasCookies(origin),
+ `${origin} should have retained its cookies when permission is set for ${permissionOrigin}.`
+ );
+ }
+
+ ok(
+ !SiteDataTestUtils.hasCookies(TRACKING_PAGE),
+ "cookie is removed after purge with no storage access permission."
+ );
+
+ PermissionTestUtils.remove(permissionOrigin, "storageAccessAPI");
+ await SiteDataTestUtils.clear();
+ }
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+}
+
+/**
+ * Test that trackers are not cleared if they are associated
+ * with an entry on the entity list that has user interaction.
+ */
+async function testUserInteraction(ownerPage) {
+ Services.prefs.setBoolPref(
+ "privacy.purge_trackers.consider_entity_list",
+ true
+ );
+ // The test URL for the entity list for annotation is
+ // itisatrap.org/?resource=example.org, so we need to
+ // add example.org as a tracker.
+ Services.prefs.setCharPref(
+ "urlclassifier.trackingAnnotationTable.testEntries",
+ "example.org"
+ );
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ // example.org and itisatrap.org are hard coded test values on the entity list.
+ const RESOURCE_PAGE = "https://example.org";
+
+ PermissionTestUtils.add(
+ ownerPage,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ SiteDataTestUtils.addToCookies({ origin: RESOURCE_PAGE });
+
+ // Add another tracker to verify we're actually purging.
+ SiteDataTestUtils.addToCookies({
+ origin: "https://another-tracking.example.net",
+ });
+
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ ok(
+ SiteDataTestUtils.hasCookies(RESOURCE_PAGE),
+ `${RESOURCE_PAGE} should have retained its cookies when permission is set for ${ownerPage}.`
+ );
+
+ ok(
+ !SiteDataTestUtils.hasCookies("https://another-tracking.example.net"),
+ "cookie is removed after purge with no storage access permission."
+ );
+
+ Services.prefs.setBoolPref(
+ "privacy.purge_trackers.consider_entity_list",
+ false
+ );
+
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ ok(
+ !SiteDataTestUtils.hasCookies(RESOURCE_PAGE),
+ `${RESOURCE_PAGE} should not have retained its cookies when permission is set for ${ownerPage} and the entity list pref is off.`
+ );
+
+ PermissionTestUtils.remove(ownerPage, "storageAccessAPI");
+ await SiteDataTestUtils.clear();
+
+ Services.prefs.clearUserPref("privacy.purge_trackers.consider_entity_list");
+ UrlClassifierTestUtils.cleanupTestTrackers();
+}
+
+/**
+ * Test that quota storage (even without cookies) is considered when purging trackers.
+ */
+async function testQuotaStorage() {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ let testCases = [
+ { localStorage: true, indexedDB: true },
+ { localStorage: false, indexedDB: true },
+ { localStorage: true, indexedDB: false },
+ ];
+
+ for (let { localStorage, indexedDB } of testCases) {
+ PermissionTestUtils.add(
+ TRACKING_PAGE,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ if (localStorage) {
+ for (let url of [
+ TRACKING_PAGE,
+ TRACKING_PAGE2,
+ BENIGN_PAGE,
+ FOREIGN_PAGE,
+ FOREIGN_PAGE2,
+ FOREIGN_PAGE3,
+ ]) {
+ SiteDataTestUtils.addToLocalStorage(url);
+ }
+ }
+
+ if (indexedDB) {
+ for (let url of [
+ TRACKING_PAGE,
+ TRACKING_PAGE2,
+ BENIGN_PAGE,
+ FOREIGN_PAGE,
+ FOREIGN_PAGE2,
+ FOREIGN_PAGE3,
+ ]) {
+ await SiteDataTestUtils.addToIndexedDB(url);
+ }
+ }
+
+ // Purge while storage access permission exists.
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ if (localStorage) {
+ ok(
+ SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE),
+ "localStorage should not have been removed while storage access permission exists."
+ );
+ }
+
+ if (indexedDB) {
+ for (let url of [
+ TRACKING_PAGE,
+ TRACKING_PAGE2,
+ FOREIGN_PAGE,
+ FOREIGN_PAGE2,
+ FOREIGN_PAGE3,
+ ]) {
+ Assert.greater(
+ await SiteDataTestUtils.getQuotaUsage(url),
+ 0,
+ `We have data for ${url}`
+ );
+ }
+ }
+
+ // Run purge after storage access permission has been removed.
+ PermissionTestUtils.remove(TRACKING_PAGE, "storageAccessAPI");
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ if (localStorage) {
+ for (let url of [
+ BENIGN_PAGE,
+ FOREIGN_PAGE,
+ FOREIGN_PAGE2,
+ FOREIGN_PAGE3,
+ ]) {
+ ok(
+ SiteDataTestUtils.hasLocalStorage(url),
+ "localStorage should not have been removed for non-tracking page."
+ );
+ }
+ for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) {
+ ok(
+ !SiteDataTestUtils.hasLocalStorage(url),
+ "localStorage should have been removed."
+ );
+ }
+ }
+
+ if (indexedDB) {
+ for (let url of [
+ BENIGN_PAGE,
+ FOREIGN_PAGE,
+ FOREIGN_PAGE2,
+ FOREIGN_PAGE3,
+ ]) {
+ Assert.greater(
+ await SiteDataTestUtils.getQuotaUsage(url),
+ 0,
+ "quota storage for non-tracking page was not deleted"
+ );
+ }
+
+ for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) {
+ Assert.equal(
+ await SiteDataTestUtils.getQuotaUsage(url),
+ 0,
+ "quota storage was deleted"
+ );
+ }
+ }
+
+ await SiteDataTestUtils.clear();
+ }
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+}
+
+/**
+ * Test that we correctly delete cookies and storage for sites
+ * with an expired interaction permission.
+ */
+async function testExpiredInteractionPermission() {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ PermissionTestUtils.add(
+ TRACKING_PAGE,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_TIME,
+ Date.now() + 500
+ );
+
+ for (let url of [
+ TRACKING_PAGE,
+ TRACKING_PAGE2,
+ FOREIGN_PAGE,
+ FOREIGN_PAGE2,
+ FOREIGN_PAGE3,
+ ]) {
+ SiteDataTestUtils.addToLocalStorage(url);
+ SiteDataTestUtils.addToCookies({ origin: url });
+ await SiteDataTestUtils.addToIndexedDB(url);
+ }
+
+ // Purge while storage access permission exists.
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ for (let url of [
+ TRACKING_PAGE,
+ TRACKING_PAGE2,
+ FOREIGN_PAGE,
+ FOREIGN_PAGE2,
+ FOREIGN_PAGE3,
+ ]) {
+ ok(
+ SiteDataTestUtils.hasCookies(url),
+ "cookie remains while storage access permission exists."
+ );
+ ok(
+ SiteDataTestUtils.hasLocalStorage(url),
+ "localStorage should not have been removed while storage access permission exists."
+ );
+ Assert.greater(
+ await SiteDataTestUtils.getQuotaUsage(url),
+ 0,
+ `We have data for ${url}`
+ );
+ }
+
+ // Run purge after storage access permission has been removed.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(c => setTimeout(c, 500));
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ // Cookie should have been removed.
+ for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) {
+ ok(
+ !SiteDataTestUtils.hasCookies(url),
+ "cookie is removed after purge with no storage access permission."
+ );
+ ok(
+ !SiteDataTestUtils.hasLocalStorage(url),
+ "localStorage should not have been removed while storage access permission exists."
+ );
+ Assert.equal(
+ await SiteDataTestUtils.getQuotaUsage(url),
+ 0,
+ "quota storage was deleted"
+ );
+ }
+
+ // Cookie should not have been removed.
+ for (let url of [FOREIGN_PAGE, FOREIGN_PAGE2, FOREIGN_PAGE3]) {
+ ok(
+ SiteDataTestUtils.hasCookies(url),
+ "cookie remains while storage access permission exists."
+ );
+ ok(
+ SiteDataTestUtils.hasLocalStorage(url),
+ "localStorage should not have been removed while storage access permission exists."
+ );
+ }
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+}
+
+/*
+ * Test that we correctly do or do not purges cookies
+ * from sites given thier cookie permissions.
+ */
+async function testNotPurgingFromAllowedWebsites() {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE });
+ SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE2 });
+
+ PermissionTestUtils.add(
+ TRACKING_PAGE,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+
+ PermissionTestUtils.add(
+ TRACKING_PAGE2,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_SESSION
+ );
+
+ ok(
+ SiteDataTestUtils.hasCookies(TRACKING_PAGE),
+ "Cookie is set to the initial state for Tracking Page 1"
+ );
+ ok(
+ SiteDataTestUtils.hasCookies(TRACKING_PAGE2),
+ "Cookie is set to the initial state for Tracking Page 2"
+ );
+
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ ok(
+ SiteDataTestUtils.hasCookies(TRACKING_PAGE),
+ "Cookie was not purged for Tracking Page 1"
+ );
+ ok(
+ !SiteDataTestUtils.hasCookies(TRACKING_PAGE2),
+ "Cookie was purged for Tracking Page 2"
+ );
+
+ PermissionTestUtils.remove(TRACKING_PAGE, "cookie");
+
+ PermissionTestUtils.remove(TRACKING_PAGE2, "cookie");
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+}
+
+/*
+ * Testing that Local Storage is not purged
+ * from sites based thier cookie permissions.
+ */
+async function testNotPurgingLocalStorage() {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ SiteDataTestUtils.addToLocalStorage(TRACKING_PAGE);
+ SiteDataTestUtils.addToLocalStorage(TRACKING_PAGE2);
+
+ PermissionTestUtils.add(
+ TRACKING_PAGE,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+
+ PermissionTestUtils.add(
+ TRACKING_PAGE2,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_SESSION
+ );
+
+ ok(
+ SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE),
+ "Local Storage is set to the initial state for Tracking Page 1"
+ );
+ ok(
+ SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE2),
+ "Local Storage is set to the initial state for Tracking Page 2"
+ );
+
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ ok(
+ SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE),
+ "Local Storage was not purged for Tracking Page 1"
+ );
+ ok(
+ !SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE2),
+ "Local Storage was not purged for Tracking Page 2"
+ );
+
+ PermissionTestUtils.remove(TRACKING_PAGE, "cookie");
+
+ PermissionTestUtils.remove(TRACKING_PAGE2, "cookie");
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+}
+
+/*
+ * Test that we correctly do or do not purges cookies
+ * from http sites given thier cookie permissions.
+ */
+async function testNotPurgingFromHTTP() {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ SiteDataTestUtils.addToCookies({ origin: HTTP_TRACKING_PAGE });
+ SiteDataTestUtils.addToCookies({
+ origin: HTTP_TRACKING_PAGE2,
+ });
+
+ PermissionTestUtils.add(
+ HTTP_TRACKING_PAGE,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+
+ PermissionTestUtils.add(
+ HTTP_TRACKING_PAGE2,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_SESSION
+ );
+
+ ok(
+ SiteDataTestUtils.hasCookies(HTTP_TRACKING_PAGE),
+ "Cookie is set to the initial state for HTTP Tracking Page 1"
+ );
+ ok(
+ SiteDataTestUtils.hasCookies(HTTP_TRACKING_PAGE2),
+ "Cookie is set to the initial state for HTTP Tracking Page 2"
+ );
+
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ ok(
+ SiteDataTestUtils.hasCookies(HTTP_TRACKING_PAGE),
+ "Cookie was not purged for HTTP Tracking Page 1"
+ );
+ ok(
+ !SiteDataTestUtils.hasCookies(HTTP_TRACKING_PAGE2),
+ "Cookie was purged for HTTP Tracking Page 2"
+ );
+
+ PermissionTestUtils.remove(HTTP_TRACKING_PAGE, "cookie");
+
+ PermissionTestUtils.remove(HTTP_TRACKING_PAGE2, "cookie");
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+}
+
+/*
+ * Test that we correctly do or do not purges local storage
+ * from http sites if https site has preserve cookies permission
+ */
+async function testNotPurgingFromDifferentScheme() {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ SiteDataTestUtils.addToLocalStorage(TRACKING_PAGE);
+ SiteDataTestUtils.addToLocalStorage(HTTP_TRACKING_PAGE);
+
+ PermissionTestUtils.add(
+ TRACKING_PAGE,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+
+ PermissionTestUtils.add(
+ HTTP_TRACKING_PAGE,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_SESSION
+ );
+
+ ok(
+ SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE),
+ "Local Storage is set to the initial state for HTTPS Tracking Page "
+ );
+ ok(
+ SiteDataTestUtils.hasLocalStorage(HTTP_TRACKING_PAGE),
+ "Local Storage is set to the initial state for HTTP Tracking Page"
+ );
+
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ ok(
+ SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE),
+ "Local Storage was not purged for HTTPS Tracking Page "
+ );
+ ok(
+ SiteDataTestUtils.hasLocalStorage(HTTP_TRACKING_PAGE),
+ "Local Storage was not purged for HTTP Tracking Page"
+ );
+
+ PermissionTestUtils.remove(TRACKING_PAGE, "cookie");
+
+ PermissionTestUtils.remove(HTTP_TRACKING_PAGE, "cookie");
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+}
+
+add_task(async function () {
+ const cookieBehaviors = [
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ];
+
+ for (let cookieBehavior of cookieBehaviors) {
+ await setupTest(cookieBehavior);
+ await testIndexedDBAndLocalStorage();
+ await testBaseDomain();
+ // example.org and itisatrap.org are hard coded test values on the entity list.
+ await testUserInteraction("https://itisatrap.org");
+ await testUserInteraction(
+ "https://itisatrap.org^firstPartyDomain=example.net"
+ );
+ await testQuotaStorage();
+ await testExpiredInteractionPermission();
+ await testNotPurgingFromAllowedWebsites();
+ await testNotPurgingLocalStorage();
+ await testNotPurgingFromHTTP();
+ await testNotPurgingFromDifferentScheme();
+ }
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js
new file mode 100644
index 0000000000..a1502373dc
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js
@@ -0,0 +1,175 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TRACKING_PAGE = "https://tracking.example.org";
+const BENIGN_PAGE = "https://example.com";
+
+const { UrlClassifierTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "PurgeTrackerService",
+ "@mozilla.org/purge-tracker-service;1",
+ "nsIPurgeTrackerService"
+);
+
+add_task(async function setup() {
+ Services.prefs.setIntPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ Services.prefs.setBoolPref("privacy.purge_trackers.enabled", true);
+ Services.prefs.setStringPref(
+ "urlclassifier.trackingAnnotationTable.testEntries",
+ "tracking.example.org"
+ );
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ // Enables us to test localStorage in xpcshell.
+ Services.prefs.setBoolPref("dom.storage.client_validation", false);
+});
+
+/**
+ * Test telemetry for cookie purging.
+ */
+add_task(async function () {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ let FIVE_DAYS = 5 * 24 * 60 * 60 * 1000;
+
+ PermissionTestUtils.add(
+ TRACKING_PAGE,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_TIME,
+ Date.now() + FIVE_DAYS
+ );
+
+ SiteDataTestUtils.addToLocalStorage(TRACKING_PAGE);
+ SiteDataTestUtils.addToCookies({ origin: BENIGN_PAGE });
+ SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE });
+ await SiteDataTestUtils.addToIndexedDB(TRACKING_PAGE);
+
+ let purgedHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "COOKIE_PURGING_ORIGINS_PURGED"
+ );
+ let notPurgedHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "COOKIE_PURGING_TRACKERS_WITH_USER_INTERACTION"
+ );
+ let remainingDaysHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "COOKIE_PURGING_TRACKERS_USER_INTERACTION_REMAINING_DAYS"
+ );
+ let intervalHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "COOKIE_PURGING_INTERVAL_HOURS"
+ );
+
+ // Purge while storage access permission exists.
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ ok(
+ SiteDataTestUtils.hasCookies(TRACKING_PAGE),
+ "cookie remains while storage access permission exists."
+ );
+ ok(
+ SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE),
+ "localStorage should not have been removed while storage access permission exists."
+ );
+ Assert.greater(
+ await SiteDataTestUtils.getQuotaUsage(TRACKING_PAGE),
+ 0,
+ `We have data for ${TRACKING_PAGE}`
+ );
+
+ TelemetryTestUtils.assertHistogram(purgedHistogram, 0, 1);
+ TelemetryTestUtils.assertHistogram(notPurgedHistogram, 1, 1);
+ TelemetryTestUtils.assertHistogram(remainingDaysHistogram, 4, 2);
+ TelemetryTestUtils.assertHistogram(intervalHistogram, 0, 1);
+
+ purgedHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "COOKIE_PURGING_ORIGINS_PURGED"
+ );
+ notPurgedHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "COOKIE_PURGING_TRACKERS_WITH_USER_INTERACTION"
+ );
+ intervalHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "COOKIE_PURGING_INTERVAL_HOURS"
+ );
+
+ // Run purge after storage access permission has been removed.
+ PermissionTestUtils.remove(TRACKING_PAGE, "storageAccessAPI");
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ ok(
+ SiteDataTestUtils.hasCookies(BENIGN_PAGE),
+ "A non-tracking page should retain cookies after purging"
+ );
+
+ // Cookie should have been removed.
+ ok(
+ !SiteDataTestUtils.hasCookies(TRACKING_PAGE),
+ "cookie is removed after purge with no storage access permission."
+ );
+ ok(
+ !SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE),
+ "localStorage should not have been removed while storage access permission exists."
+ );
+ Assert.equal(
+ await SiteDataTestUtils.getQuotaUsage(TRACKING_PAGE),
+ 0,
+ "quota storage was deleted"
+ );
+
+ TelemetryTestUtils.assertHistogram(purgedHistogram, 1, 1);
+ Assert.equal(
+ notPurgedHistogram.snapshot().sum,
+ 0,
+ "no origins with user interaction"
+ );
+ TelemetryTestUtils.assertHistogram(intervalHistogram, 0, 1);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+/**
+ * Test counting correctly across cookies batches
+ */
+add_task(async function () {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ // Enforce deleting the same origin twice by adding two cookies and setting
+ // the max number of cookies per batch to 1.
+ SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE, name: "cookie1" });
+ SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE, name: "cookie2" });
+ Services.prefs.setIntPref("privacy.purge_trackers.max_purge_count", 1);
+
+ let purgedHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "COOKIE_PURGING_ORIGINS_PURGED"
+ );
+
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ // Cookie should have been removed.
+ await TestUtils.waitForCondition(
+ () => !SiteDataTestUtils.hasCookies(TRACKING_PAGE),
+ "cookie is removed after purge."
+ );
+
+ TelemetryTestUtils.assertHistogram(purgedHistogram, 1, 1);
+
+ Services.prefs.clearUserPref("privacy.purge_trackers.max_purge_count");
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_authhttp.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_authhttp.js
new file mode 100644
index 0000000000..0492f5ff2a
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_authhttp.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+CookieXPCShellUtils.init(this);
+
+function Requestor() {}
+Requestor.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIInterfaceRequestor",
+ "nsIAuthPrompt2",
+ ]),
+
+ getInterface(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt2)) {
+ return this;
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ promptAuth(channel, level, authInfo) {
+ Assert.equal("secret", authInfo.realm);
+ // No passwords in the URL -> nothing should be prefilled
+ Assert.equal(authInfo.username, "");
+ Assert.equal(authInfo.password, "");
+ Assert.equal(authInfo.domain, "");
+
+ authInfo.username = "guest";
+ authInfo.password = "guest";
+
+ return true;
+ },
+
+ asyncPromptAuth(chan, cb, ctx, lvl, info) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+};
+
+let observer = channel => {
+ if (
+ !(channel instanceof Ci.nsIHttpChannel && channel.URI.host === "localhost")
+ ) {
+ return;
+ }
+ channel.notificationCallbacks = new Requestor();
+};
+Services.obs.addObserver(observer, "http-on-modify-request");
+
+add_task(async () => {
+ do_get_profile();
+
+ Services.prefs.setBoolPref("network.predictor.enabled", false);
+ Services.prefs.setBoolPref("network.predictor.enable-prefetch", false);
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setIntPref("network.auth.subresource-http-auth-allow", 2);
+
+ for (let test of [true, false]) {
+ Cc["@mozilla.org/network/http-auth-manager;1"]
+ .getService(Ci.nsIHttpAuthManager)
+ .clearAll();
+
+ await new Promise(resolve =>
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve)
+ );
+
+ info("Enabling network state partitioning");
+ Services.prefs.setBoolPref("privacy.partition.network_state", test);
+
+ const httpserv = new HttpServer();
+ httpserv.registerPathHandler("/auth", (metadata, response) => {
+ // btoa("guest:guest"), but that function is not available here
+ const expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q=";
+
+ let body;
+ if (
+ metadata.hasHeader("Authorization") &&
+ metadata.getHeader("Authorization") == expectedHeader
+ ) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+
+ body = "success";
+ } else {
+ // didn't know guest:guest, failure
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+
+ body = "failed";
+ }
+
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ httpserv.start(-1);
+ const URL = "http://localhost:" + httpserv.identity.primaryPort;
+
+ const httpHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+ ].getService(Ci.nsIHttpProtocolHandler);
+
+ const contentPage = await CookieXPCShellUtils.loadContentPage(
+ URL + "/auth?r=" + Math.random()
+ );
+ await contentPage.close();
+
+ let key;
+ if (test) {
+ key = `^partitionKey=%28http%2Clocalhost%2C${httpserv.identity.primaryPort}%29:http://localhost:${httpserv.identity.primaryPort}`;
+ } else {
+ key = `:http://localhost:${httpserv.identity.primaryPort}`;
+ }
+
+ Assert.equal(httpHandler.authCacheKeys.includes(key), true, "Key found!");
+
+ await new Promise(resolve => httpserv.stop(resolve));
+ }
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_clientAuthRemember.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_clientAuthRemember.js
new file mode 100644
index 0000000000..ba2f6c6894
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_clientAuthRemember.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+let cars = Cc["@mozilla.org/security/clientAuthRememberService;1"].getService(
+ Ci.nsIClientAuthRememberService
+);
+let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+);
+
+function getOAWithPartitionKey(
+ { scheme = "https", topLevelBaseDomain, port = null } = {},
+ originAttributes = {}
+) {
+ if (!topLevelBaseDomain || !scheme) {
+ return originAttributes;
+ }
+
+ return {
+ ...originAttributes,
+ partitionKey: `(${scheme},${topLevelBaseDomain}${port ? `,${port}` : ""})`,
+ };
+}
+
+// These are not actual server and client certs. The ClientAuthRememberService
+// does not care which certs we store decisions for, as long as they're valid.
+let [clientCert] = certDB.getCerts();
+
+function addSecurityInfo({ host, topLevelBaseDomain, originAttributes = {} }) {
+ let attrs = getOAWithPartitionKey({ topLevelBaseDomain }, originAttributes);
+ cars.rememberDecisionScriptable(host, attrs, clientCert);
+}
+
+function testSecurityInfo({
+ host,
+ topLevelBaseDomain,
+ originAttributes = {},
+ expected = true,
+}) {
+ let attrs = getOAWithPartitionKey({ topLevelBaseDomain }, originAttributes);
+
+ let messageSuffix = `for ${host}`;
+ if (topLevelBaseDomain) {
+ messageSuffix += ` partitioned under ${topLevelBaseDomain}`;
+ }
+
+ let hasRemembered = cars.hasRememberedDecisionScriptable(host, attrs, {});
+
+ Assert.equal(
+ hasRemembered,
+ expected,
+ `CAR ${expected ? "is set" : "is not set"} ${messageSuffix}`
+ );
+}
+
+function addTestEntries() {
+ let entries = [
+ { host: "example.net" },
+ { host: "test.example.net" },
+ { host: "example.org" },
+ { host: "example.com", topLevelBaseDomain: "example.net" },
+ {
+ host: "test.example.net",
+ topLevelBaseDomain: "example.org",
+ },
+ {
+ host: "foo.example.com",
+ originAttributes: {
+ privateBrowsingId: 1,
+ },
+ },
+ ];
+
+ info("Add test state");
+ entries.forEach(addSecurityInfo);
+ info("Ensure we have the correct state initially");
+ entries.forEach(testSecurityInfo);
+}
+
+add_task(async () => {
+ addTestEntries();
+
+ info("Should not be set for unrelated host");
+ [undefined, "example.org", "example.net", "example.com"].forEach(
+ topLevelBaseDomain =>
+ testSecurityInfo({
+ host: "mochit.test",
+ topLevelBaseDomain,
+ expected: false,
+ })
+ );
+
+ info("Should not be set for unrelated subdomain");
+ testSecurityInfo({ host: "foo.example.net", expected: false });
+
+ info("Should not be set for unpartitioned first party");
+ testSecurityInfo({
+ host: "example.com",
+ expected: false,
+ });
+
+ info("Should not be set under different first party");
+ testSecurityInfo({
+ host: "example.com",
+ topLevelBaseDomain: "example.org",
+ expected: false,
+ });
+ testSecurityInfo({
+ host: "test.example.net",
+ topLevelBaseDomain: "example.com",
+ expected: false,
+ });
+
+ info("Should not be set in partitioned context");
+ ["example.com", "example.net", "example.org", "mochi.test"].forEach(
+ topLevelBaseDomain =>
+ testSecurityInfo({
+ host: "foo.example.com",
+ topLevelBaseDomain,
+ expected: false,
+ })
+ );
+
+ // Cleanup
+ cars.clearRememberedDecisions();
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_font.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_font.js
new file mode 100644
index 0000000000..46e230ec3e
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_font.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+CookieXPCShellUtils.init(this);
+
+let gHits = 0;
+
+add_task(async function () {
+ do_get_profile();
+
+ info("Disable predictor and accept all");
+ Services.prefs.setBoolPref("network.predictor.enabled", false);
+ Services.prefs.setBoolPref("network.predictor.enable-prefetch", false);
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ const server = CookieXPCShellUtils.createServer({
+ hosts: ["example.org", "foo.com", "bar.com"],
+ });
+
+ server.registerFile(
+ "/font.woff",
+ do_get_file("data/font.woff"),
+ (_, response) => {
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ gHits++;
+ }
+ );
+
+ server.registerPathHandler("/font", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ let body = `
+ <style type="text/css">
+ @font-face {
+ font-family: foo;
+ src: url("http://example.org/font.woff") format('woff');
+ }
+ body { font-family: foo }
+ </style>
+ <iframe src="http://example.org/font-iframe">
+ </iframe>`;
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ server.registerPathHandler("/font-iframe", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ let body = `
+ <style type="text/css">
+ @font-face {
+ font-family: foo;
+ src: url("http://example.org/font.woff") format('woff');
+ }
+ body { font-family: foo }
+ </style>`;
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ const tests = [
+ {
+ prefValue: true,
+ hitsCount: 5,
+ },
+ {
+ prefValue: false,
+ // The font in page B/C is CORS, the channel will be flagged with
+ // nsIRequest::LOAD_ANONYMOUS.
+ // The flag makes the font in A and B/C use different cache key.
+ hitsCount: 2,
+ },
+ ];
+
+ for (let test of tests) {
+ info("Clear network caches");
+ Services.cache2.clear();
+
+ info("Reset the hits count");
+ gHits = 0;
+
+ info("Enabling network state partitioning");
+ Services.prefs.setBoolPref(
+ "privacy.partition.network_state",
+ test.prefValue
+ );
+
+ info("Let's load a page with origin A");
+ let contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/font"
+ );
+ await contentPage.close();
+
+ info("Let's load a page with origin B");
+ contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://foo.com/font"
+ );
+ await contentPage.close();
+
+ info("Let's load a page with origin C");
+ contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://bar.com/font"
+ );
+ await contentPage.close();
+
+ Assert.equal(gHits, test.hitsCount, "The number of hits match");
+ }
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js
new file mode 100644
index 0000000000..7492d2267a
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+CookieXPCShellUtils.init(this);
+
+let gHits = 0;
+
+add_task(async function () {
+ do_get_profile();
+
+ info("Disable predictor and accept all");
+ Services.prefs.setBoolPref("network.predictor.enabled", false);
+ Services.prefs.setBoolPref("network.predictor.enable-prefetch", false);
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ const server = CookieXPCShellUtils.createServer({
+ hosts: ["example.org", "foo.com"],
+ });
+ server.registerPathHandler("/image.png", (metadata, response) => {
+ gHits++;
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "image/png", false);
+ var body = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAEUlEQVQImWP4z8AAQTAamQkAhpcI+DeMzFcAAAAASUVORK5CYII="
+ );
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ server.registerPathHandler("/image", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ var body = `<img src="http://example.org/image.png">`;
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ const tests = [
+ {
+ prefValue: true,
+ hitsCount: 2,
+ },
+ {
+ prefValue: false,
+ hitsCount: 1,
+ },
+ ];
+
+ for (let test of tests) {
+ info("Clear image and network caches");
+ let imageCache = Cc["@mozilla.org/image/tools;1"]
+ .getService(Ci.imgITools)
+ .getImgCacheForDocument(null);
+ imageCache.clearCache(true); // true=chrome
+ imageCache.clearCache(false); // false=content
+ Services.cache2.clear();
+
+ info("Reset the hits count");
+ gHits = 0;
+
+ info("Enabling network state partitioning");
+ Services.prefs.setBoolPref(
+ "privacy.partition.network_state",
+ test.prefValue
+ );
+
+ info("Let's load a page with origin A");
+ let contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/image"
+ );
+ await contentPage.close();
+
+ info("Let's load a page with origin B");
+ contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://foo.com/image"
+ );
+ await contentPage.close();
+
+ Assert.equal(gHits, test.hitsCount, "The number of hits match");
+ }
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js
new file mode 100644
index 0000000000..f7ec4cc8e3
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js
@@ -0,0 +1,178 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+// Small red image.
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+let gHints = 0;
+
+CookieXPCShellUtils.init(this);
+
+function countMatchingCacheEntries(cacheEntries, domain, path) {
+ return cacheEntries
+ .map(entry => entry.uri.asciiSpec)
+ .filter(spec => spec.includes(domain))
+ .filter(spec => spec.includes(path)).length;
+}
+
+async function checkCache(originAttributes) {
+ const loadContextInfo = Services.loadContextInfo.custom(
+ false,
+ originAttributes
+ );
+
+ const data = await new Promise(resolve => {
+ let cacheEntries = [];
+ let cacheVisitor = {
+ onCacheStorageInfo(num, consumption) {},
+ onCacheEntryInfo(uri, idEnhance) {
+ cacheEntries.push({ uri, idEnhance });
+ },
+ onCacheEntryVisitCompleted() {
+ resolve(cacheEntries);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]),
+ };
+ // Visiting the disk cache also visits memory storage so we do not
+ // need to use Services.cache2.memoryCacheStorage() here.
+ let storage = Services.cache2.diskCacheStorage(loadContextInfo);
+ storage.asyncVisitStorage(cacheVisitor, true);
+ });
+
+ let foundEntryCount = countMatchingCacheEntries(
+ data,
+ "example.org",
+ "image.png"
+ );
+ Assert.greater(
+ foundEntryCount,
+ 0,
+ `Cache entries expected for image.png and OA=${originAttributes}`
+ );
+}
+
+add_task(async () => {
+ do_get_profile();
+
+ Services.prefs.setBoolPref("network.prefetch-next", true);
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ const server = CookieXPCShellUtils.createServer({
+ hosts: ["example.org", "foo.com"],
+ });
+
+ server.registerPathHandler("/image.png", (metadata, response) => {
+ gHints++;
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "image/png", false);
+ response.write(IMG_BYTES);
+ });
+
+ server.registerPathHandler("/prefetch", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ var body = `<html><head></head><body><script>
+ const link = document.createElement("link")
+ link.setAttribute("rel", "prefetch");
+ link.setAttribute("href", "http://example.org/image.png");
+ document.head.appendChild(link);
+ link.onload = () => {
+ const img = document.createElement("IMG");
+ img.src = "http://example.org/image.png";
+ document.body.appendChild(img);
+ fetch("/done").then(() => {});
+ }
+ </script></body></html>`;
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ const tests = [
+ {
+ // 2 hints because we have 2 different top-level origins, loading the
+ // same resource. This will end up creating 2 separate cache entries.
+ hints: 2,
+ originAttributes: { partitionKey: "(http,example.org)" },
+ prefValue: true,
+ },
+ {
+ // 1 hint because, with network-state isolation, the cache entry will be
+ // reused for the second loading, even if the top-level origins are
+ // different.
+ hints: 1,
+ originAttributes: {},
+ prefValue: false,
+ },
+ ];
+
+ for (let test of tests) {
+ await new Promise(resolve =>
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve)
+ );
+
+ info("Reset the counter");
+ gHints = 0;
+
+ info("Enabling network state partitioning");
+ Services.prefs.setBoolPref(
+ "privacy.partition.network_state",
+ test.prefValue
+ );
+
+ let complete = new Promise(resolve => {
+ server.registerPathHandler("/done", (metadata, response) => {
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ var body = "OK";
+ response.bodyOutputStream.write(body, body.length);
+ resolve();
+ });
+ });
+
+ info("Let's load a page with origin A");
+ let contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/prefetch"
+ );
+
+ await complete;
+ await checkCache(test.originAttributes);
+ await contentPage.close();
+
+ complete = new Promise(resolve => {
+ server.registerPathHandler("/done", (metadata, response) => {
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ var body = "OK";
+ response.bodyOutputStream.write(body, body.length);
+ resolve();
+ });
+ });
+
+ info("Let's load a page with origin B");
+ contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://foo.com/prefetch"
+ );
+
+ await complete;
+ await checkCache(test.originAttributes);
+ await contentPage.close();
+
+ Assert.equal(
+ gHints,
+ test.hints,
+ "We have the current number of requests with pref " + test.prefValue
+ );
+ }
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js
new file mode 100644
index 0000000000..20158f2f7a
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js
@@ -0,0 +1,187 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+let gHints = 0;
+
+CookieXPCShellUtils.init(this);
+
+function countMatchingCacheEntries(cacheEntries, domain, path) {
+ return cacheEntries
+ .map(entry => entry.uri.asciiSpec)
+ .filter(spec => spec.includes(domain))
+ .filter(spec => spec.includes(path)).length;
+}
+
+async function checkCache(originAttributes) {
+ const loadContextInfo = Services.loadContextInfo.custom(
+ false,
+ originAttributes
+ );
+
+ const data = await new Promise(resolve => {
+ let cacheEntries = [];
+ let cacheVisitor = {
+ onCacheStorageInfo(num, consumption) {},
+ onCacheEntryInfo(uri, idEnhance) {
+ cacheEntries.push({ uri, idEnhance });
+ },
+ onCacheEntryVisitCompleted() {
+ resolve(cacheEntries);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]),
+ };
+ // Visiting the disk cache also visits memory storage so we do not
+ // need to use Services.cache2.memoryCacheStorage() here.
+ let storage = Services.cache2.diskCacheStorage(loadContextInfo);
+ storage.asyncVisitStorage(cacheVisitor, true);
+ });
+
+ let foundEntryCount = countMatchingCacheEntries(
+ data,
+ "example.org",
+ "style.css"
+ );
+ Assert.greater(
+ foundEntryCount,
+ 0,
+ `Cache entries expected for style.css and OA=${originAttributes}`
+ );
+}
+
+add_task(async () => {
+ do_get_profile();
+
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ const server = CookieXPCShellUtils.createServer({
+ hosts: ["example.org", "foo.com"],
+ });
+
+ server.registerPathHandler("/empty", (metadata, response) => {
+ var body = "<h1>Hello!</h1>";
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ server.registerPathHandler("/style.css", (metadata, response) => {
+ gHints++;
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ var body = "* { color: red }";
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ server.registerPathHandler("/preload", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ var body = `<html><head></head><body><script>
+ const link = document.createElement("link")
+ link.setAttribute("rel", "preload");
+ link.setAttribute("as", "style");
+ link.setAttribute("href", "http://example.org/style.css");
+ document.head.appendChild(link);
+ link.onload = () => {
+ fetch("/done").then(() => {});
+ };
+ </script></body></html>`;
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ const tests = [
+ {
+ // 2 hints because we have 2 different top-level origins, loading the
+ // same resource. This will end up creating 2 separate cache entries.
+ hints: 2,
+ prefValue: true,
+ originAttributes: { partitionKey: "(http,example.org)" },
+ },
+ {
+ // 1 hint because, with network-state isolation, the cache entry will be
+ // reused for the second loading, even if the top-level origins are
+ // different.
+ hints: 1,
+ originAttributes: {},
+ prefValue: false,
+ },
+ ];
+
+ for (let test of tests) {
+ await new Promise(resolve =>
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve)
+ );
+
+ info("Reset the shared sheets");
+ let contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/empty"
+ );
+
+ await contentPage.spawn([], () =>
+ // eslint-disable-next-line no-undef
+ content.windowUtils.clearSharedStyleSheetCache()
+ );
+
+ await contentPage.close();
+
+ info("Reset the counter");
+ gHints = 0;
+
+ info("Enabling network state partitioning");
+ Services.prefs.setBoolPref(
+ "privacy.partition.network_state",
+ test.prefValue
+ );
+
+ let complete = new Promise(resolve => {
+ server.registerPathHandler("/done", (metadata, response) => {
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ var body = "OK";
+ response.bodyOutputStream.write(body, body.length);
+ resolve();
+ });
+ });
+
+ info("Let's load a page with origin A");
+ contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/preload"
+ );
+
+ await complete;
+ await checkCache(test.originAttributes);
+ await contentPage.close();
+
+ complete = new Promise(resolve => {
+ server.registerPathHandler("/done", (metadata, response) => {
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ var body = "OK";
+ response.bodyOutputStream.write(body, body.length);
+ resolve();
+ });
+ });
+
+ info("Let's load a page with origin B");
+ contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://foo.com/preload"
+ );
+
+ await complete;
+ await checkCache(test.originAttributes);
+ await contentPage.close();
+
+ Assert.equal(
+ gHints,
+ test.hints,
+ "We have the current number of requests with pref " + test.prefValue
+ );
+ }
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js b/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js
new file mode 100644
index 0000000000..651b817f85
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js
@@ -0,0 +1,538 @@
+/* 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/. */
+
+// Note: This test may cause intermittents if run at exactly midnight.
+
+"use strict";
+
+const { Sqlite } = ChromeUtils.importESModule(
+ "resource://gre/modules/Sqlite.sys.mjs"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "TrackingDBService",
+ "@mozilla.org/tracking-db-service;1",
+ "nsITrackingDBService"
+);
+
+ChromeUtils.defineLazyGetter(this, "DB_PATH", function () {
+ return PathUtils.join(PathUtils.profileDir, "protections.sqlite");
+});
+
+const SQL = {
+ insertCustomTimeEvent:
+ "INSERT INTO events (type, count, timestamp)" +
+ "VALUES (:type, :count, date(:timestamp));",
+
+ selectAllEntriesOfType: "SELECT * FROM events WHERE type = :type;",
+
+ selectAll: "SELECT * FROM events",
+};
+
+// Emulate the content blocking log. We do not record the url key, nor
+// do we use the aggregated event number (the last element in the array).
+const LOG = {
+ "https://1.example.com": [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1],
+ ],
+ "https://2.example.com": [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT, true, 1],
+ ],
+ "https://3.example.com": [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT, true, 2],
+ ],
+ "https://4.example.com": [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, true, 3],
+ ],
+ "https://5.example.com": [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, true, 1],
+ ],
+ // Cookie blocked for other reason, then identified as a tracker
+ "https://6.example.com": [
+ [
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL |
+ Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT,
+ true,
+ 4,
+ ],
+ ],
+ "https://7.example.com": [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER, true, 1],
+ ],
+ "https://8.example.com": [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT, true, 1],
+ ],
+
+ // The contents below should not add to the database.
+ // Cookie loaded but not blocked.
+ "https://10.example.com": [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, true, 1],
+ ],
+ // Tracker cookie loaded but not blocked.
+ "https://11.unblocked.example.com": [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_TRACKER, true, 1],
+ ],
+ // Social tracker cookie loaded but not blocked.
+ "https://12.example.com": [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER, true, 1],
+ ],
+ // Cookie blocked for other reason (not a tracker)
+ "https://13.example.com": [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION, true, 2],
+ ],
+ // Fingerprinters set to block, but this one has an exception
+ "https://14.example.com": [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT, false, 1],
+ ],
+ // Two fingerprinters replaced with a shims script, should be treated as blocked
+ // and increment the counter.
+ "https://15.example.com": [
+ [Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT, true, 1],
+ ],
+ "https://16.example.com": [
+ [Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT, true, 1],
+ ],
+ "https://17.example.com": [
+ [
+ Ci.nsIWebProgressListener.STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING,
+ true,
+ 1,
+ ],
+ ],
+};
+
+do_get_profile();
+
+Services.prefs.setBoolPref("browser.contentblocking.database.enabled", true);
+Services.prefs.setBoolPref(
+ "privacy.socialtracking.block_cookies.enabled",
+ true
+);
+Services.prefs.setBoolPref(
+ "privacy.trackingprotection.fingerprinting.enabled",
+ true
+);
+Services.prefs.setBoolPref("privacy.fingerprintingProtection", true);
+Services.prefs.setBoolPref(
+ "browser.contentblocking.cfr-milestone.enabled",
+ true
+);
+Services.prefs.setIntPref(
+ "browser.contentblocking.cfr-milestone.update-interval",
+ 0
+);
+Services.prefs.setStringPref(
+ "browser.contentblocking.cfr-milestone.milestones",
+ "[1000, 5000, 10000, 25000, 100000, 500000]"
+);
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.contentblocking.database.enabled");
+ Services.prefs.clearUserPref("privacy.socialtracking.block_cookies.enabled");
+ Services.prefs.clearUserPref(
+ "privacy.trackingprotection.fingerprinting.enabled"
+ );
+ Services.prefs.clearUserPref("privacy.fingerprintingProtection");
+ Services.prefs.clearUserPref("browser.contentblocking.cfr-milestone.enabled");
+ Services.prefs.clearUserPref(
+ "browser.contentblocking.cfr-milestone.update-interval"
+ );
+ Services.prefs.clearUserPref(
+ "browser.contentblocking.cfr-milestone.milestones"
+ );
+});
+
+// This tests that data is added successfully, different types of events should get
+// their own entries, when the type is the same they should be aggregated. Events
+// that are not blocking events should not be recorded. Cookie blocking events
+// should only be recorded if we can identify the cookie as a tracking cookie.
+add_task(async function test_save_and_delete() {
+ await TrackingDBService.saveEvents(JSON.stringify(LOG));
+
+ // Peek in the DB to make sure we have the right data.
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+ // Make sure the items table was created.
+ ok(await db.tableExists("events"), "events table exists");
+
+ // make sure we have the correct contents in the database
+ let rows = await db.execute(SQL.selectAll);
+ equal(
+ rows.length,
+ 6,
+ "Events that should not be saved have not been, length is 6"
+ );
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.TRACKERS_ID,
+ });
+ equal(rows.length, 1, "Only one day has had tracker entries, length is 1");
+ let count = rows[0].getResultByName("count");
+ equal(count, 1, "there is only one tracker entry");
+
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ });
+ equal(rows.length, 1, "Only one day has had cookies entries, length is 1");
+ count = rows[0].getResultByName("count");
+ equal(count, 3, "Cookie entries were aggregated");
+
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ });
+ equal(
+ rows.length,
+ 1,
+ "Only one day has had cryptominer entries, length is 1"
+ );
+ count = rows[0].getResultByName("count");
+ equal(count, 1, "there is only one cryptominer entry");
+
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.FINGERPRINTERS_ID,
+ });
+ equal(
+ rows.length,
+ 1,
+ "Only one day has had fingerprinters entries, length is 1"
+ );
+ count = rows[0].getResultByName("count");
+ equal(count, 3, "there are three fingerprinter entries");
+
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.SOCIAL_ID,
+ });
+ equal(rows.length, 1, "Only one day has had social entries, length is 1");
+ count = rows[0].getResultByName("count");
+ equal(count, 2, "there are two social entries");
+
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.SUSPICIOUS_FINGERPRINTERS_ID,
+ });
+ equal(
+ rows.length,
+ 1,
+ "Only one day has had suspicious fingerprinting entries, length is 1"
+ );
+ count = rows[0].getResultByName("count");
+ equal(count, 1, "there is one suspicious fingerprinting entry");
+
+ // Use the TrackingDBService API to delete the data.
+ await TrackingDBService.clearAll();
+ // Make sure the data was deleted.
+ rows = await db.execute(SQL.selectAll);
+ equal(rows.length, 0, "length is 0");
+ await db.close();
+});
+
+// This tests that content blocking events encountered on the same day get aggregated,
+// and those on different days get seperate entries
+add_task(async function test_timestamp_aggragation() {
+ // This creates the schema.
+ await TrackingDBService.saveEvents(JSON.stringify({}));
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+
+ let yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
+ let today = new Date().toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKERS_ID,
+ count: 4,
+ timestamp: yesterday,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ count: 3,
+ timestamp: yesterday,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.FINGERPRINTERS_ID,
+ count: 2,
+ timestamp: yesterday,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ count: 1,
+ timestamp: yesterday,
+ });
+
+ // Add some events for today which must get aggregated
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKERS_ID,
+ count: 2,
+ timestamp: today,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ count: 2,
+ timestamp: today,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.FINGERPRINTERS_ID,
+ count: 2,
+ timestamp: today,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ count: 2,
+ timestamp: today,
+ });
+
+ // Add new events, they will have today's timestamp.
+ await TrackingDBService.saveEvents(JSON.stringify(LOG));
+
+ // Ensure events that are inserted today are not aggregated with past events.
+ let rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.TRACKERS_ID,
+ });
+ equal(rows.length, 2, "Tracker entries for today and yesterday, length is 2");
+ for (let i = 0; i < rows.length; i++) {
+ let count = rows[i].getResultByName("count");
+ if (i == 0) {
+ equal(count, 4, "Yesterday's count is 4");
+ } else if (i == 1) {
+ equal(count, 3, "Today's count is 3, new entries were aggregated");
+ }
+ }
+
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ });
+ equal(
+ rows.length,
+ 2,
+ "Cryptominer entries for today and yesterday, length is 2"
+ );
+ for (let i = 0; i < rows.length; i++) {
+ let count = rows[i].getResultByName("count");
+ if (i == 0) {
+ equal(count, 3, "Yesterday's count is 3");
+ } else if (i == 1) {
+ equal(count, 3, "Today's count is 3, new entries were aggregated");
+ }
+ }
+
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.FINGERPRINTERS_ID,
+ });
+ equal(
+ rows.length,
+ 2,
+ "Fingerprinter entries for today and yesterday, length is 2"
+ );
+ for (let i = 0; i < rows.length; i++) {
+ let count = rows[i].getResultByName("count");
+ if (i == 0) {
+ equal(count, 2, "Yesterday's count is 2");
+ } else if (i == 1) {
+ equal(count, 5, "Today's count is 5, new entries were aggregated");
+ }
+ }
+
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ });
+ equal(
+ rows.length,
+ 2,
+ "Tracking Cookies entries for today and yesterday, length is 2"
+ );
+ for (let i = 0; i < rows.length; i++) {
+ let count = rows[i].getResultByName("count");
+ if (i == 0) {
+ equal(count, 1, "Yesterday's count is 1");
+ } else if (i == 1) {
+ equal(count, 5, "Today's count is 5, new entries were aggregated");
+ }
+ }
+
+ // Use the TrackingDBService API to delete the data.
+ await TrackingDBService.clearAll();
+ // Make sure the data was deleted.
+ rows = await db.execute(SQL.selectAll);
+ equal(rows.length, 0, "length is 0");
+ await db.close();
+});
+
+let addEventsToDB = async db => {
+ let d = new Date(1521009000000);
+ let date = d.toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ count: 3,
+ timestamp: date,
+ });
+
+ date = new Date(d - 2 * 24 * 60 * 60 * 1000).toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKERS_ID,
+ count: 2,
+ timestamp: date,
+ });
+
+ date = new Date(d - 3 * 24 * 60 * 60 * 1000).toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ count: 2,
+ timestamp: date,
+ });
+
+ date = new Date(d - 4 * 24 * 60 * 60 * 1000).toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ count: 2,
+ timestamp: date,
+ });
+
+ date = new Date(d - 9 * 24 * 60 * 60 * 1000).toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.FINGERPRINTERS_ID,
+ count: 2,
+ timestamp: date,
+ });
+};
+
+// This tests that TrackingDBService.getEventsByDateRange can accept two timestamps in unix epoch time
+// and return entries that occur within the timestamps, rounded to the nearest day and inclusive.
+add_task(async function test_getEventsByDateRange() {
+ // This creates the schema.
+ await TrackingDBService.saveEvents(JSON.stringify({}));
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+ await addEventsToDB(db);
+
+ let d = new Date(1521009000000);
+ let daysBefore1 = new Date(d - 24 * 60 * 60 * 1000);
+ let daysBefore4 = new Date(d - 4 * 24 * 60 * 60 * 1000);
+ let daysBefore9 = new Date(d - 9 * 24 * 60 * 60 * 1000);
+
+ let events = await TrackingDBService.getEventsByDateRange(daysBefore1, d);
+ equal(
+ events.length,
+ 1,
+ "There is 1 event entry between the date and one day before, inclusive"
+ );
+
+ events = await TrackingDBService.getEventsByDateRange(daysBefore4, d);
+ equal(
+ events.length,
+ 4,
+ "There is 4 event entries between the date and four days before, inclusive"
+ );
+
+ events = await TrackingDBService.getEventsByDateRange(
+ daysBefore9,
+ daysBefore4
+ );
+ equal(
+ events.length,
+ 2,
+ "There is 2 event entries between nine and four days before, inclusive"
+ );
+
+ await TrackingDBService.clearAll();
+ await db.close();
+});
+
+// This tests that TrackingDBService.sumAllEvents returns the number of
+// tracking events in the database, and can handle 0 entries.
+add_task(async function test_sumAllEvents() {
+ // This creates the schema.
+ await TrackingDBService.saveEvents(JSON.stringify({}));
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+
+ let sum = await TrackingDBService.sumAllEvents();
+ equal(sum, 0, "There have been 0 events recorded");
+
+ // populate the database
+ await addEventsToDB(db);
+
+ sum = await TrackingDBService.sumAllEvents();
+ equal(sum, 11, "There have been 11 events recorded");
+
+ await TrackingDBService.clearAll();
+ await db.close();
+});
+
+// This tests that TrackingDBService.getEarliestRecordedDate returns the
+// earliest date recorded and can handle 0 entries.
+add_task(async function test_getEarliestRecordedDate() {
+ // This creates the schema.
+ await TrackingDBService.saveEvents(JSON.stringify({}));
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+
+ let timestamp = await TrackingDBService.getEarliestRecordedDate();
+ equal(timestamp, null, "There is no earliest recorded date");
+
+ // populate the database
+ await addEventsToDB(db);
+ let d = new Date(1521009000000);
+ let daysBefore9 = new Date(d - 9 * 24 * 60 * 60 * 1000)
+ .toISOString()
+ .split("T")[0];
+
+ timestamp = await TrackingDBService.getEarliestRecordedDate();
+ let date = new Date(timestamp).toISOString().split("T")[0];
+ equal(date, daysBefore9, "The earliest recorded event is nine days before.");
+
+ await TrackingDBService.clearAll();
+ await db.close();
+});
+
+// This tests that a message to CFR is sent when the amount of saved trackers meets a milestone
+add_task(async function test_sendMilestoneNotification() {
+ let milestones = JSON.parse(
+ Services.prefs.getStringPref(
+ "browser.contentblocking.cfr-milestone.milestones"
+ )
+ );
+ // This creates the schema.
+ await TrackingDBService.saveEvents(JSON.stringify({}));
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+ // save number of trackers equal to the first milestone
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ count: milestones[0],
+ timestamp: new Date().toISOString(),
+ });
+
+ let awaitNotification = TestUtils.topicObserved(
+ "SiteProtection:ContentBlockingMilestone"
+ );
+
+ // trigger a "save" event to compare the trackers with the milestone.
+ await TrackingDBService.saveEvents(
+ JSON.stringify({
+ "https://1.example.com": [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1],
+ ],
+ })
+ );
+ await awaitNotification;
+
+ await TrackingDBService.clearAll();
+ await db.close();
+});
+
+// Ensure we don't record suspicious fingerprinting if the fingerprinting
+// protection is disabled.
+add_task(async function test_noSuspiciousFingerprintingWithFPPDisabled() {
+ Services.prefs.setBoolPref("privacy.fingerprintingProtection", false);
+
+ await TrackingDBService.saveEvents(JSON.stringify(LOG));
+
+ // Peek in the DB to make sure we have the right data.
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+ // Make sure the items table was created.
+ ok(await db.tableExists("events"), "events table exists");
+
+ let rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.SUSPICIOUS_FINGERPRINTERS_ID,
+ });
+ equal(
+ rows.length,
+ 0,
+ "Should be no suspicious entry if the fingerprinting protection is disabled"
+ );
+
+ // Use the TrackingDBService API to delete the data.
+ await TrackingDBService.clearAll();
+ await db.close();
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_validate_strip_on_share_list.js b/toolkit/components/antitracking/test/xpcshell/test_validate_strip_on_share_list.js
new file mode 100644
index 0000000000..48a0bd4682
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_validate_strip_on_share_list.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { JsonSchema } = ChromeUtils.importESModule(
+ "resource://gre/modules/JsonSchema.sys.mjs"
+);
+
+let stripOnShareList;
+
+// Fetching strip on share list
+add_setup(async function () {
+ /* globals fetch */
+ let response = await fetch(
+ "chrome://global/content/antitracking/StripOnShare.json"
+ );
+ if (!response.ok) {
+ throw new Error(
+ "Error fetching strip-on-share strip list" + response.status
+ );
+ }
+ stripOnShareList = await response.json();
+});
+
+// Check if the Strip on Share list contains any duplicate params
+add_task(async function test_check_duplicates() {
+ let stripOnShareParams = stripOnShareList;
+
+ const allQueryParams = [];
+
+ for (const domain in stripOnShareParams) {
+ for (let param in stripOnShareParams[domain].queryParams) {
+ allQueryParams.push(stripOnShareParams[domain].queryParams[param]);
+ }
+ }
+
+ let setOfParams = new Set(allQueryParams);
+
+ if (setOfParams.size != allQueryParams.length) {
+ let setToCheckDupes = new Set();
+ let dupeList = new Set();
+ for (const domain in stripOnShareParams) {
+ for (let param in stripOnShareParams[domain].queryParams) {
+ let tempParam = stripOnShareParams[domain].queryParams[param];
+
+ if (setToCheckDupes.has(tempParam)) {
+ dupeList.add(tempParam);
+ } else {
+ setToCheckDupes.add(tempParam);
+ }
+ }
+ }
+
+ Assert.equal(
+ setOfParams.size,
+ allQueryParams.length,
+ "There are duplicates rules. The duplicate rules are " + [...dupeList]
+ );
+ }
+
+ Assert.equal(
+ setOfParams.size,
+ allQueryParams.length,
+ "There are no duplicates rules."
+ );
+});
+
+// Validate the format of Strip on Share list with Schema
+add_task(async function test_check_schema() {
+ let schema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ type: "object",
+ properties: {
+ queryParams: {
+ type: "array",
+ items: { type: "string" },
+ },
+ topLevelSites: {
+ type: "array",
+ items: { type: "string" },
+ },
+ },
+ required: ["queryParams", "topLevelSites"],
+ },
+ required: ["global"],
+ };
+
+ let stripOnShareParams = stripOnShareList;
+ let validator = new JsonSchema.Validator(schema);
+ let { valid, errors } = validator.validate(stripOnShareParams);
+
+ if (!valid) {
+ info("validation errors: " + JSON.stringify(errors, null, 2));
+ }
+
+ Assert.ok(valid, "Strip on share JSON is valid");
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_view_source.js b/toolkit/components/antitracking/test/xpcshell/test_view_source.js
new file mode 100644
index 0000000000..ebd70cf476
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_view_source.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+CookieXPCShellUtils.init(this);
+
+let gCookieHits = 0;
+let gLoadingHits = 0;
+
+add_task(async function () {
+ do_get_profile();
+
+ info("Disable predictor and accept all");
+ Services.prefs.setBoolPref("network.predictor.enabled", false);
+ Services.prefs.setBoolPref("network.predictor.enable-prefetch", false);
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+ Services.prefs.setIntPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ );
+
+ const server = CookieXPCShellUtils.createServer({
+ hosts: ["example.org"],
+ });
+ server.registerPathHandler("/test", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ if (
+ request.hasHeader("Cookie") &&
+ request.getHeader("Cookie") == "foo=bar"
+ ) {
+ gCookieHits++;
+ } else {
+ response.setHeader("Set-Cookie", "foo=bar");
+ }
+
+ gLoadingHits++;
+ var body = "<html></html>";
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ info("Reset the hits count");
+ gCookieHits = 0;
+ gLoadingHits = 0;
+
+ info("Let's load a page");
+ let contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/test?1"
+ );
+ await contentPage.close();
+
+ Assert.equal(gCookieHits, 0, "The number of cookie hits match");
+ Assert.equal(gLoadingHits, 1, "The number of loading hits match");
+
+ info("Let's load the source of the page again to see if it loads from cache");
+ contentPage = await CookieXPCShellUtils.loadContentPage(
+ "view-source:http://example.org/test?1"
+ );
+ await contentPage.close();
+
+ Assert.equal(gCookieHits, 0, "The number of cookie hits match");
+ Assert.equal(gLoadingHits, 1, "The number of loading hits match");
+
+ info(
+ "Let's load the source of the page without hitting the cache to see if the cookie is sent properly"
+ );
+ contentPage = await CookieXPCShellUtils.loadContentPage(
+ "view-source:http://example.org/test?2"
+ );
+ await contentPage.close();
+
+ Assert.equal(gCookieHits, 1, "The number of cookie hits match");
+ Assert.equal(gLoadingHits, 2, "The number of loading hits match");
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/xpcshell.toml b/toolkit/components/antitracking/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..86f524ab89
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/xpcshell.toml
@@ -0,0 +1,52 @@
+[DEFAULT]
+head = "head.js ../../../../components/url-classifier/tests/unit/head_urlclassifier.js"
+prefs = ["dom.security.https_first=false"] #Disable https-first because of explicit http/https testing
+
+["test_ExceptionListService.js"]
+
+["test_cookie_behavior.js"]
+
+["test_getPartitionKeyFromURL.js"]
+skip-if = ["socketprocess_networking"] # Bug 1759035
+
+["test_purge_trackers.js"]
+skip-if = [
+ "win10_2009", # Bug 1718292
+ "win11_2009", # Bug 1797751
+]
+run-sequentially = "very high failure rate in parallel"
+
+["test_purge_trackers_telemetry.js"]
+
+["test_staticPartition_authhttp.js"]
+skip-if = ["socketprocess_networking"] # Bug 1759035
+
+["test_staticPartition_clientAuthRemember.js"]
+
+["test_staticPartition_font.js"]
+support-files = ["data/font.woff"]
+skip-if = [
+ "os == 'linux' && !debug", # Bug 1760086
+ "apple_silicon", # bug 1729551
+ "os == 'mac' && bits == 64 && !debug", # Bug 1652119
+ "os == 'win' && bits == 64 && !debug", # Bug 1652119
+ "socketprocess_networking", # Bug 1759035
+]
+run-sequentially = "very high failure rate in parallel"
+
+["test_staticPartition_image.js"]
+skip-if = ["socketprocess_networking"] # Bug 1759035
+
+["test_staticPartition_prefetch.js"]
+skip-if = ["socketprocess_networking"] # Bug 1759035
+
+["test_staticPartition_preload.js"]
+skip-if = ["socketprocess_networking"] # Bug 1759035
+
+["test_tracking_db_service.js"]
+skip-if = ["os == 'android'"] # Bug 1697936
+
+["test_validate_strip_on_share_list.js"]
+
+["test_view_source.js"]
+skip-if = ["socketprocess_networking"] # Bug 1759035 (not as common on win, perma on linux/osx)