summaryrefslogtreecommitdiffstats
path: root/browser/modules/test/unit
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /browser/modules/test/unit
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/modules/test/unit')
-rw-r--r--browser/modules/test/unit/test_E10SUtils_nested_URIs.js90
-rw-r--r--browser/modules/test/unit/test_HomePage.js92
-rw-r--r--browser/modules/test/unit/test_HomePage_ignore.js135
-rw-r--r--browser/modules/test/unit/test_InstallationTelemetry.js284
-rw-r--r--browser/modules/test/unit/test_LaterRun.js242
-rw-r--r--browser/modules/test/unit/test_PartnerLinkAttribution.js54
-rw-r--r--browser/modules/test/unit/test_PingCentre.js194
-rw-r--r--browser/modules/test/unit/test_ProfileCounter.js239
-rw-r--r--browser/modules/test/unit/test_Sanitizer_interrupted.js139
-rw-r--r--browser/modules/test/unit/test_SiteDataManager.js277
-rw-r--r--browser/modules/test/unit/test_SiteDataManagerContainers.js140
-rw-r--r--browser/modules/test/unit/test_SitePermissions.js401
-rw-r--r--browser/modules/test/unit/test_SitePermissions_temporary.js710
-rw-r--r--browser/modules/test/unit/test_TabUnloader.js449
-rw-r--r--browser/modules/test/unit/test_discovery.js138
-rw-r--r--browser/modules/test/unit/xpcshell.ini23
16 files changed, 3607 insertions, 0 deletions
diff --git a/browser/modules/test/unit/test_E10SUtils_nested_URIs.js b/browser/modules/test/unit/test_E10SUtils_nested_URIs.js
new file mode 100644
index 0000000000..5ebcac114f
--- /dev/null
+++ b/browser/modules/test/unit/test_E10SUtils_nested_URIs.js
@@ -0,0 +1,90 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+
+const { E10SUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/E10SUtils.sys.mjs"
+);
+
+var TEST_PREFERRED_REMOTE_TYPES = [
+ E10SUtils.WEB_REMOTE_TYPE,
+ E10SUtils.NOT_REMOTE,
+ "fakeRemoteType",
+];
+
+// These test cases give a nestedURL and a plainURL that should always load in
+// the same remote type. By making these tests comparisons, they should work
+// with any pref combination.
+var TEST_CASES = [
+ {
+ nestedURL: "jar:file:///some.file!/",
+ plainURL: "file:///some.file",
+ },
+ {
+ nestedURL: "jar:jar:file:///some.file!/!/",
+ plainURL: "file:///some.file",
+ },
+ {
+ nestedURL: "jar:http://some.site/file!/",
+ plainURL: "http://some.site/file",
+ },
+ {
+ nestedURL: "view-source:http://some.site",
+ plainURL: "http://some.site",
+ },
+ {
+ nestedURL: "view-source:file:///some.file",
+ plainURL: "file:///some.file",
+ },
+ {
+ nestedURL: "view-source:about:home",
+ plainURL: "about:home",
+ },
+ {
+ nestedURL: "view-source:about:robots",
+ plainURL: "about:robots",
+ },
+ {
+ nestedURL: "view-source:pcast:http://some.site",
+ plainURL: "http://some.site",
+ },
+];
+
+function run_test() {
+ for (let testCase of TEST_CASES) {
+ for (let preferredRemoteType of TEST_PREFERRED_REMOTE_TYPES) {
+ let plainUri = Services.io.newURI(testCase.plainURL);
+ let plainRemoteType = E10SUtils.getRemoteTypeForURIObject(plainUri, {
+ multiProcess: true,
+ remoteSubFrames: false,
+ preferredRemoteType,
+ });
+
+ let nestedUri = Services.io.newURI(testCase.nestedURL);
+ let nestedRemoteType = E10SUtils.getRemoteTypeForURIObject(nestedUri, {
+ multiProcess: true,
+ remoteSubFrames: false,
+ preferredRemoteType,
+ });
+
+ let nestedStr = nestedUri.scheme + ":";
+ do {
+ nestedUri = nestedUri.QueryInterface(Ci.nsINestedURI).innerURI;
+ if (nestedUri.scheme == "about") {
+ nestedStr += nestedUri.spec;
+ break;
+ }
+
+ nestedStr += nestedUri.scheme + ":";
+ } while (nestedUri instanceof Ci.nsINestedURI);
+
+ let plainStr =
+ plainUri.scheme == "about" ? plainUri.spec : plainUri.scheme + ":";
+ equal(
+ nestedRemoteType,
+ plainRemoteType,
+ `Check that ${nestedStr} loads in same remote type as ${plainStr}` +
+ ` with preferred remote type: ${preferredRemoteType}`
+ );
+ }
+ }
+}
diff --git a/browser/modules/test/unit/test_HomePage.js b/browser/modules/test/unit/test_HomePage.js
new file mode 100644
index 0000000000..0fb1030f1d
--- /dev/null
+++ b/browser/modules/test/unit/test_HomePage.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ HomePage: "resource:///modules/HomePage.jsm",
+});
+
+const HOMEPAGE_IGNORELIST = "homepage-urls";
+
+/**
+ * Provides a basic set of remote settings for use in tests.
+ */
+async function setupRemoteSettings() {
+ const settings = await RemoteSettings("hijack-blocklists");
+ sinon.stub(settings, "get").returns([
+ {
+ id: HOMEPAGE_IGNORELIST,
+ matches: ["ignore=me"],
+ _status: "synced",
+ },
+ ]);
+}
+
+add_task(async function setup() {
+ await setupRemoteSettings();
+});
+
+add_task(function test_HomePage() {
+ Assert.ok(
+ !HomePage.overridden,
+ "Homepage should not be overriden by default."
+ );
+ let newvalue = "about:blank|about:newtab";
+ HomePage.safeSet(newvalue);
+ Assert.ok(HomePage.overridden, "Homepage should be overriden after set()");
+ Assert.equal(HomePage.get(), newvalue, "Homepage should be ${newvalue}");
+ Assert.notEqual(
+ HomePage.getDefault(),
+ newvalue,
+ "Homepage should be ${newvalue}"
+ );
+ HomePage.reset();
+ Assert.ok(
+ !HomePage.overridden,
+ "Homepage should not be overriden by after reset."
+ );
+ Assert.equal(
+ HomePage.get(),
+ HomePage.getDefault(),
+ "Homepage and default should be equal after reset."
+ );
+});
+
+add_task(function test_readLocalizedHomepage() {
+ let newvalue = "data:text/plain,browser.startup.homepage%3Dabout%3Alocalized";
+ let complexvalue = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(
+ Ci.nsIPrefLocalizedString
+ );
+ complexvalue.data = newvalue;
+ Services.prefs
+ .getDefaultBranch(null)
+ .setComplexValue(
+ "browser.startup.homepage",
+ Ci.nsIPrefLocalizedString,
+ complexvalue
+ );
+ Assert.ok(!HomePage.overridden, "Complex value only works as default");
+ Assert.equal(HomePage.get(), "about:localized", "Get value from bundle");
+});
+
+add_task(function test_recoverEmptyHomepage() {
+ Assert.ok(
+ !HomePage.overridden,
+ "Homepage should not be overriden by default."
+ );
+ Services.prefs.setStringPref("browser.startup.homepage", "");
+ Assert.ok(HomePage.overridden, "Homepage is overriden with empty string.");
+ Assert.equal(HomePage.get(), HomePage.getDefault(), "Recover is default");
+ Assert.ok(!HomePage.overridden, "Recover should have set default");
+});
diff --git a/browser/modules/test/unit/test_HomePage_ignore.js b/browser/modules/test/unit/test_HomePage_ignore.js
new file mode 100644
index 0000000000..26d384bea7
--- /dev/null
+++ b/browser/modules/test/unit/test_HomePage_ignore.js
@@ -0,0 +1,135 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ HomePage: "resource:///modules/HomePage.jsm",
+});
+
+const HOMEPAGE_IGNORELIST = "homepage-urls";
+
+/**
+ * Provides a basic set of remote settings for use in tests.
+ */
+async function setupRemoteSettings() {
+ const settings = await RemoteSettings("hijack-blocklists");
+ sinon.stub(settings, "get").returns([
+ {
+ id: HOMEPAGE_IGNORELIST,
+ matches: ["ignore=me", "ignoreCASE=ME"],
+ _status: "synced",
+ },
+ ]);
+}
+
+add_task(async function setup() {
+ await setupRemoteSettings();
+});
+
+add_task(async function test_initWithIgnoredPageCausesReset() {
+ // Set the preference direct as the set() would block us.
+ Services.prefs.setStringPref(
+ "browser.startup.homepage",
+ "http://bad/?ignore=me"
+ );
+ Assert.ok(HomePage.overridden, "Should have overriden the homepage");
+
+ await HomePage.delayedStartup();
+
+ Assert.ok(
+ !HomePage.overridden,
+ "Should no longer be overriding the homepage."
+ );
+ Assert.equal(
+ HomePage.get(),
+ HomePage.getDefault(),
+ "Should have reset to the default preference"
+ );
+
+ TelemetryTestUtils.assertEvents(
+ [{ object: "ignore", value: "saved_reset" }],
+ {
+ category: "homepage",
+ method: "preference",
+ }
+ );
+});
+
+add_task(async function test_updateIgnoreListCausesReset() {
+ Services.prefs.setStringPref(
+ "browser.startup.homepage",
+ "http://bad/?new=ignore"
+ );
+ Assert.ok(HomePage.overridden, "Should have overriden the homepage");
+
+ // Simulate an ignore list update.
+ await RemoteSettings("hijack-blocklists").emit("sync", {
+ data: {
+ current: [
+ {
+ id: HOMEPAGE_IGNORELIST,
+ schema: 1553857697843,
+ last_modified: 1553859483588,
+ matches: ["ignore=me", "ignoreCASE=ME", "new=ignore"],
+ },
+ ],
+ },
+ });
+
+ Assert.ok(
+ !HomePage.overridden,
+ "Should no longer be overriding the homepage."
+ );
+ Assert.equal(
+ HomePage.get(),
+ HomePage.getDefault(),
+ "Should have reset to the default preference"
+ );
+ TelemetryTestUtils.assertEvents(
+ [{ object: "ignore", value: "saved_reset" }],
+ {
+ category: "homepage",
+ method: "preference",
+ }
+ );
+});
+
+async function testSetIgnoredUrl(url) {
+ Assert.ok(!HomePage.overriden, "Should not be overriding the homepage");
+
+ await HomePage.set(url);
+
+ Assert.equal(
+ HomePage.get(),
+ HomePage.getDefault(),
+ "Should still have the default homepage."
+ );
+ Assert.ok(!HomePage.overriden, "Should not be overriding the homepage.");
+ TelemetryTestUtils.assertEvents(
+ [{ object: "ignore", value: "set_blocked" }],
+ {
+ category: "homepage",
+ method: "preference",
+ }
+ );
+}
+
+add_task(async function test_setIgnoredUrl() {
+ await testSetIgnoredUrl("http://bad/?ignore=me");
+});
+
+add_task(async function test_setIgnoredUrl_case() {
+ await testSetIgnoredUrl("http://bad/?Ignorecase=me");
+});
diff --git a/browser/modules/test/unit/test_InstallationTelemetry.js b/browser/modules/test/unit/test_InstallationTelemetry.js
new file mode 100644
index 0000000000..64d9964cbe
--- /dev/null
+++ b/browser/modules/test/unit/test_InstallationTelemetry.js
@@ -0,0 +1,284 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { AttributionIOUtils } = ChromeUtils.importESModule(
+ "resource:///modules/AttributionCode.sys.mjs"
+);
+const { BrowserUsageTelemetry } = ChromeUtils.import(
+ "resource:///modules/BrowserUsageTelemetry.jsm"
+);
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+ChromeUtils.defineESModuleGetters(this, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+});
+
+const TIMESTAMP_PREF = "app.installation.timestamp";
+
+function encodeUtf16(str) {
+ const buf = new ArrayBuffer(str.length * 2);
+ const utf16 = new Uint16Array(buf);
+ for (let i = 0; i < str.length; i++) {
+ utf16[i] = str.charCodeAt(i);
+ }
+ return new Uint8Array(buf);
+}
+
+// Returns Promise
+function writeJsonUtf16(fileName, obj) {
+ const str = JSON.stringify(obj);
+ return IOUtils.write(fileName, encodeUtf16(str));
+}
+
+async function runReport(
+ dataFile,
+ installType,
+ { clearTS, setTS, assertRejects, expectExtra, expectTS, msixPrefixes }
+) {
+ // Setup timestamp
+ if (clearTS) {
+ Services.prefs.clearUserPref(TIMESTAMP_PREF);
+ }
+ if (typeof setTS == "string") {
+ Services.prefs.setStringPref(TIMESTAMP_PREF, setTS);
+ }
+
+ // Init events
+ Services.telemetry.clearEvents();
+
+ // Exercise reportInstallationTelemetry
+ if (typeof assertRejects != "undefined") {
+ await Assert.rejects(
+ BrowserUsageTelemetry.reportInstallationTelemetry(dataFile),
+ assertRejects
+ );
+ } else if (!msixPrefixes) {
+ await BrowserUsageTelemetry.reportInstallationTelemetry(dataFile);
+ } else {
+ await BrowserUsageTelemetry.reportInstallationTelemetry(
+ dataFile,
+ msixPrefixes
+ );
+ }
+
+ // Check events
+ TelemetryTestUtils.assertEvents(
+ expectExtra
+ ? [{ object: installType, value: null, extra: expectExtra }]
+ : [],
+ { category: "installation", method: "first_seen" },
+ { clear: false }
+ );
+ // Provenance Data is currently only supported on Windows.
+ if (AppConstants.platform == "win") {
+ let provenanceExtra = {
+ data_exists: "true",
+ file_system: "NTFS",
+ ads_exists: "true",
+ security_zone: "3",
+ refer_url_exist: "true",
+ refer_url_moz: "true",
+ host_url_exist: "true",
+ host_url_moz: "true",
+ };
+ TelemetryTestUtils.assertEvents(
+ expectExtra
+ ? [{ object: installType, value: null, extra: provenanceExtra }]
+ : [],
+ { category: "installation", method: "first_seen_prov_ext" }
+ );
+ } else {
+ TelemetryTestUtils.assertEvents(
+ expectExtra ? [{ object: installType, value: null, extra: {} }] : [],
+ { category: "installation", method: "first_seen_prov_ext" }
+ );
+ }
+
+ // Check timestamp
+ if (typeof expectTS == "string") {
+ Assert.equal(expectTS, Services.prefs.getStringPref(TIMESTAMP_PREF));
+ }
+}
+
+add_setup(function setup() {
+ let origReadUTF8 = AttributionIOUtils.readUTF8;
+ registerCleanupFunction(() => {
+ AttributionIOUtils.readUTF8 = origReadUTF8;
+ });
+ AttributionIOUtils.readUTF8 = async path => {
+ return `
+[Mozilla]
+fileSystem=NTFS
+zoneIdFileSize=194
+zoneIdBufferLargeEnough=true
+zoneIdTruncated=false
+
+[MozillaZoneIdentifierStartSentinel]
+[ZoneTransfer]
+ZoneId=3
+ReferrerUrl=https://mozilla.org/
+HostUrl=https://download-installer.cdn.mozilla.net/pub/firefox/nightly/latest-mozilla-central-l10n/Firefox%20Installer.en-US.exe
+`;
+ };
+});
+
+let condition = {
+ skip_if: () =>
+ AppConstants.platform !== "win" ||
+ !Services.sysinfo.getProperty("hasWinPackageId"),
+};
+add_task(condition, async function testInstallationTelemetryMSIX() {
+ // Unfortunately, we have no way to inject different installation ping data
+ // into the system in a way that doesn't just completely override the code
+ // under test - so other than a basic test of the happy path, there's
+ // nothing we can do here.
+ let msixExtra = {
+ version: AppConstants.MOZ_APP_VERSION,
+ build_id: AppConstants.MOZ_BULIDID,
+ admin_user: "false",
+ from_msi: "false",
+ silent: "false",
+ default_path: "true",
+ install_existed: "false",
+ other_inst: "false",
+ other_msix_inst: "false",
+ profdir_existed: "false",
+ };
+
+ await runReport("fake", "msix", {
+ expectExtra: msixExtra,
+ });
+});
+condition = {
+ skip_if: () =>
+ AppConstants.platform === "win" &&
+ Services.sysinfo.getProperty("hasWinPackageId"),
+};
+add_task(condition, async function testInstallationTelemetry() {
+ let dataFilePath = await IOUtils.createUniqueFile(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ "installation-telemetry-test-data" + Math.random() + ".json"
+ );
+ let dataFile = new FileUtils.File(dataFilePath);
+
+ registerCleanupFunction(async () => {
+ try {
+ await IOUtils.remove(dataFilePath);
+ } catch (ex) {
+ // Ignore remove failure, file may not exist by now
+ }
+
+ Services.prefs.clearUserPref(TIMESTAMP_PREF);
+ });
+
+ // Test with normal stub data
+ let stubData = {
+ version: "99.0abc",
+ build_id: "123",
+ installer_type: "stub",
+ admin_user: true,
+ install_existed: false,
+ profdir_existed: false,
+ install_timestamp: "0",
+ };
+ let stubExtra = {
+ version: "99.0abc",
+ build_id: "123",
+ admin_user: "true",
+ install_existed: "false",
+ other_inst: "false",
+ other_msix_inst: "false",
+ profdir_existed: "false",
+ };
+
+ await writeJsonUtf16(dataFilePath, stubData);
+ await runReport(dataFile, "stub", {
+ clearTS: true,
+ expectExtra: stubExtra,
+ expectTS: "0",
+ });
+
+ // Check that it doesn't generate another event when the timestamp is unchanged
+ await runReport(dataFile, "stub", { expectTS: "0" });
+
+ // New timestamp
+ stubData.install_timestamp = "1";
+ await writeJsonUtf16(dataFilePath, stubData);
+ await runReport(dataFile, "stub", {
+ expectExtra: stubExtra,
+ expectTS: "1",
+ });
+
+ // Test with normal full data
+ let fullData = {
+ version: "99.0abc",
+ build_id: "123",
+ installer_type: "full",
+ admin_user: false,
+ install_existed: true,
+ profdir_existed: true,
+ silent: false,
+ from_msi: false,
+ default_path: true,
+
+ install_timestamp: "1",
+ };
+ let fullExtra = {
+ version: "99.0abc",
+ build_id: "123",
+ admin_user: "false",
+ install_existed: "true",
+ other_inst: "false",
+ other_msix_inst: "false",
+ profdir_existed: "true",
+ silent: "false",
+ from_msi: "false",
+ default_path: "true",
+ };
+
+ await writeJsonUtf16(dataFilePath, fullData);
+ await runReport(dataFile, "full", {
+ clearTS: true,
+ expectExtra: fullExtra,
+ expectTS: "1",
+ });
+
+ // Check that it doesn't generate another event when the timestamp is unchanged
+ await runReport(dataFile, "full", { expectTS: "1" });
+
+ // New timestamp and a check to make sure we can find installed MSIX packages
+ // by overriding the prefixes a bit further down.
+ fullData.install_timestamp = "2";
+ // This check only works on Windows 10 and above
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
+ fullExtra.other_msix_inst = "true";
+ }
+ await writeJsonUtf16(dataFilePath, fullData);
+ await runReport(dataFile, "full", {
+ expectExtra: fullExtra,
+ expectTS: "2",
+ msixPrefixes: ["Microsoft"],
+ });
+
+ // Missing field
+ delete fullData.install_existed;
+ fullData.install_timestamp = "3";
+ await writeJsonUtf16(dataFilePath, fullData);
+ await runReport(dataFile, "full", { assertRejects: /install_existed/ });
+
+ // Malformed JSON
+ await IOUtils.write(dataFilePath, encodeUtf16("hello"));
+ await runReport(dataFile, "stub", {
+ assertRejects: /unexpected character/,
+ });
+
+ // Missing file, should return with no exception
+ await IOUtils.remove(dataFilePath);
+ await runReport(dataFile, "stub", { setTS: "3", expectTS: "3" });
+});
diff --git a/browser/modules/test/unit/test_LaterRun.js b/browser/modules/test/unit/test_LaterRun.js
new file mode 100644
index 0000000000..d78fde2414
--- /dev/null
+++ b/browser/modules/test/unit/test_LaterRun.js
@@ -0,0 +1,242 @@
+"use strict";
+
+const kEnabledPref = "browser.laterrun.enabled";
+const kPagePrefRoot = "browser.laterrun.pages.";
+const kSessionCountPref = "browser.laterrun.bookkeeping.sessionCount";
+const kProfileCreationTime = "browser.laterrun.bookkeeping.profileCreationTime";
+
+const { LaterRun } = ChromeUtils.import("resource:///modules/LaterRun.jsm");
+
+Services.prefs.setBoolPref(kEnabledPref, true);
+const { updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+updateAppInfo();
+
+add_task(async function test_page_applies() {
+ Services.prefs.setCharPref(
+ kPagePrefRoot + "test_LaterRun_unittest.url",
+ "https://www.mozilla.org/%VENDOR%/%NAME%/%ID%/%VERSION%/"
+ );
+ Services.prefs.setIntPref(
+ kPagePrefRoot + "test_LaterRun_unittest.minimumHoursSinceInstall",
+ 10
+ );
+ Services.prefs.setIntPref(
+ kPagePrefRoot + "test_LaterRun_unittest.minimumSessionCount",
+ 3
+ );
+
+ let pages = LaterRun.readPages();
+ // We have to filter the pages because it's possible Firefox ships with other URLs
+ // that get included in this test.
+ pages = pages.filter(
+ page => page.pref == kPagePrefRoot + "test_LaterRun_unittest."
+ );
+ Assert.equal(pages.length, 1, "Got 1 page");
+ let page = pages[0];
+ Assert.equal(
+ page.pref,
+ kPagePrefRoot + "test_LaterRun_unittest.",
+ "Should know its own pref"
+ );
+ Assert.equal(
+ page.minimumHoursSinceInstall,
+ 10,
+ "Needs to have 10 hours since install"
+ );
+ Assert.equal(page.minimumSessionCount, 3, "Needs to have 3 sessions");
+ Assert.equal(page.requireBoth, false, "Either requirement is enough");
+ let expectedURL =
+ "https://www.mozilla.org/" +
+ Services.appinfo.vendor +
+ "/" +
+ Services.appinfo.name +
+ "/" +
+ Services.appinfo.ID +
+ "/" +
+ Services.appinfo.version +
+ "/";
+ Assert.equal(page.url, expectedURL, "URL is stored correctly");
+
+ Assert.ok(
+ page.applies({ hoursSinceInstall: 1, sessionCount: 3 }),
+ "Applies when session count has been met."
+ );
+ Assert.ok(
+ page.applies({ hoursSinceInstall: 1, sessionCount: 4 }),
+ "Applies when session count has been exceeded."
+ );
+ Assert.ok(
+ page.applies({ hoursSinceInstall: 10, sessionCount: 2 }),
+ "Applies when total session time has been met."
+ );
+ Assert.ok(
+ page.applies({ hoursSinceInstall: 20, sessionCount: 2 }),
+ "Applies when total session time has been exceeded."
+ );
+ Assert.ok(
+ page.applies({ hoursSinceInstall: 10, sessionCount: 3 }),
+ "Applies when both time and session count have been met."
+ );
+ Assert.ok(
+ !page.applies({ hoursSinceInstall: 1, sessionCount: 1 }),
+ "Does not apply when neither time and session count have been met."
+ );
+
+ page.requireBoth = true;
+
+ Assert.ok(
+ !page.applies({ hoursSinceInstall: 1, sessionCount: 3 }),
+ "Does not apply when only session count has been met."
+ );
+ Assert.ok(
+ !page.applies({ hoursSinceInstall: 1, sessionCount: 4 }),
+ "Does not apply when only session count has been exceeded."
+ );
+ Assert.ok(
+ !page.applies({ hoursSinceInstall: 10, sessionCount: 2 }),
+ "Does not apply when only total session time has been met."
+ );
+ Assert.ok(
+ !page.applies({ hoursSinceInstall: 20, sessionCount: 2 }),
+ "Does not apply when only total session time has been exceeded."
+ );
+ Assert.ok(
+ page.applies({ hoursSinceInstall: 10, sessionCount: 3 }),
+ "Applies when both time and session count have been met."
+ );
+ Assert.ok(
+ !page.applies({ hoursSinceInstall: 1, sessionCount: 1 }),
+ "Does not apply when neither time and session count have been met."
+ );
+
+ // Check that pages that have run never apply:
+ Services.prefs.setBoolPref(
+ kPagePrefRoot + "test_LaterRun_unittest.hasRun",
+ true
+ );
+ page.requireBoth = false;
+
+ Assert.ok(
+ !page.applies({ hoursSinceInstall: 1, sessionCount: 3 }),
+ "Does not apply when page has already run (sessionCount equal)."
+ );
+ Assert.ok(
+ !page.applies({ hoursSinceInstall: 1, sessionCount: 4 }),
+ "Does not apply when page has already run (sessionCount exceeding)."
+ );
+ Assert.ok(
+ !page.applies({ hoursSinceInstall: 10, sessionCount: 2 }),
+ "Does not apply when page has already run (hoursSinceInstall equal)."
+ );
+ Assert.ok(
+ !page.applies({ hoursSinceInstall: 20, sessionCount: 2 }),
+ "Does not apply when page has already run (hoursSinceInstall exceeding)."
+ );
+ Assert.ok(
+ !page.applies({ hoursSinceInstall: 10, sessionCount: 3 }),
+ "Does not apply when page has already run (both criteria equal)."
+ );
+ Assert.ok(
+ !page.applies({ hoursSinceInstall: 1, sessionCount: 1 }),
+ "Does not apply when page has already run (both criteria insufficient anyway)."
+ );
+
+ clearAllPagePrefs();
+});
+
+add_task(async function test_get_URL() {
+ Services.prefs.setIntPref(
+ kProfileCreationTime,
+ Math.floor((Date.now() - 11 * 60 * 60 * 1000) / 1000)
+ );
+ Services.prefs.setCharPref(
+ kPagePrefRoot + "test_LaterRun_unittest.url",
+ "https://www.mozilla.org/"
+ );
+ Services.prefs.setIntPref(
+ kPagePrefRoot + "test_LaterRun_unittest.minimumHoursSinceInstall",
+ 10
+ );
+ Services.prefs.setIntPref(
+ kPagePrefRoot + "test_LaterRun_unittest.minimumSessionCount",
+ 3
+ );
+ let pages = LaterRun.readPages();
+ // We have to filter the pages because it's possible Firefox ships with other URLs
+ // that get included in this test.
+ pages = pages.filter(
+ page => page.pref == kPagePrefRoot + "test_LaterRun_unittest."
+ );
+ Assert.equal(pages.length, 1, "Should only be 1 matching page");
+ let page = pages[0];
+ let url;
+ do {
+ url = LaterRun.getURL();
+ // We have to loop because it's possible Firefox ships with other URLs that get triggered by
+ // this test.
+ } while (url && url != "https://www.mozilla.org/");
+ Assert.equal(
+ url,
+ "https://www.mozilla.org/",
+ "URL should be as expected when prefs are set."
+ );
+ Assert.ok(
+ Services.prefs.prefHasUserValue(
+ kPagePrefRoot + "test_LaterRun_unittest.hasRun"
+ ),
+ "Should have set pref"
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(kPagePrefRoot + "test_LaterRun_unittest.hasRun"),
+ "Should have set pref to true"
+ );
+ Assert.ok(page.hasRun, "Other page objects should know it has run, too.");
+
+ clearAllPagePrefs();
+});
+
+add_task(async function test_insecure_urls() {
+ Services.prefs.setCharPref(
+ kPagePrefRoot + "test_LaterRun_unittest.url",
+ "http://www.mozilla.org/"
+ );
+ Services.prefs.setIntPref(
+ kPagePrefRoot + "test_LaterRun_unittest.minimumHoursSinceInstall",
+ 10
+ );
+ Services.prefs.setIntPref(
+ kPagePrefRoot + "test_LaterRun_unittest.minimumSessionCount",
+ 3
+ );
+ let pages = LaterRun.readPages();
+ // We have to filter the pages because it's possible Firefox ships with other URLs
+ // that get triggered in this test.
+ pages = pages.filter(
+ page => page.pref == kPagePrefRoot + "test_LaterRun_unittest."
+ );
+ Assert.equal(pages.length, 0, "URL with non-https scheme should get ignored");
+ clearAllPagePrefs();
+});
+
+add_task(async function test_dynamic_pref_getter_setter() {
+ delete LaterRun._sessionCount;
+ Services.prefs.setIntPref(kSessionCountPref, 0);
+ Assert.equal(LaterRun.sessionCount, 0, "Should start at 0");
+
+ LaterRun.sessionCount++;
+ Assert.equal(LaterRun.sessionCount, 1, "Should increment.");
+ Assert.equal(
+ Services.prefs.getIntPref(kSessionCountPref),
+ 1,
+ "Should update pref"
+ );
+});
+
+function clearAllPagePrefs() {
+ let allChangedPrefs = Services.prefs.getChildList(kPagePrefRoot);
+ for (let pref of allChangedPrefs) {
+ Services.prefs.clearUserPref(pref);
+ }
+}
diff --git a/browser/modules/test/unit/test_PartnerLinkAttribution.js b/browser/modules/test/unit/test_PartnerLinkAttribution.js
new file mode 100644
index 0000000000..79053fbb07
--- /dev/null
+++ b/browser/modules/test/unit/test_PartnerLinkAttribution.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { PartnerLinkAttribution, CONTEXTUAL_SERVICES_PING_TYPES } =
+ ChromeUtils.importESModule(
+ "resource:///modules/PartnerLinkAttribution.sys.mjs"
+ );
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const FAKE_PING = { tile_id: 1, position: 1 };
+
+let sandbox;
+let stub;
+
+add_task(function setup() {
+ sandbox = sinon.createSandbox();
+ stub = sandbox.stub(
+ PartnerLinkAttribution._pingCentre,
+ "sendStructuredIngestionPing"
+ );
+ stub.returns(200);
+
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+});
+
+add_task(function test_sendContextualService_success() {
+ for (const type of Object.values(CONTEXTUAL_SERVICES_PING_TYPES)) {
+ PartnerLinkAttribution.sendContextualServicesPing(FAKE_PING, type);
+
+ Assert.ok(stub.calledOnce, `Should send the ping for ${type}`);
+
+ const [payload, endpoint] = stub.firstCall.args;
+ Assert.ok(!!payload.context_id, "Should add context_id to the payload");
+ Assert.ok(
+ endpoint.includes(type),
+ "Should include the ping type in the endpoint URL"
+ );
+ stub.resetHistory();
+ }
+});
+
+add_task(function test_rejectUnknownPingType() {
+ PartnerLinkAttribution.sendContextualServicesPing(FAKE_PING, "unknown-type");
+
+ Assert.ok(stub.notCalled, "Should not send the ping with unknown ping type");
+});
diff --git a/browser/modules/test/unit/test_PingCentre.js b/browser/modules/test/unit/test_PingCentre.js
new file mode 100644
index 0000000000..b022030196
--- /dev/null
+++ b/browser/modules/test/unit/test_PingCentre.js
@@ -0,0 +1,194 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { PingCentre, PingCentreConstants } = ChromeUtils.import(
+ "resource:///modules/PingCentre.jsm"
+);
+const { TelemetryEnvironment } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+);
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { UpdateUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/UpdateUtils.sys.mjs"
+);
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const FAKE_PING = { event: "fake_event", value: "fake_value", locale: "en-US" };
+const FAKE_ENDPOINT = "https://www.test.com";
+
+let pingCentre;
+let sandbox;
+let fogInitd = false;
+
+function _setUp() {
+ Services.prefs.setBoolPref(PingCentreConstants.TELEMETRY_PREF, true);
+ Services.prefs.setBoolPref(PingCentreConstants.FHR_UPLOAD_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(PingCentreConstants.LOGGING_PREF, true);
+ sandbox.restore();
+ if (fogInitd) {
+ Services.fog.testResetFOG();
+ }
+}
+
+add_setup(function setup() {
+ sandbox = sinon.createSandbox();
+ _setUp();
+ pingCentre = new PingCentre({ topic: "test_topic" });
+
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ Services.prefs.clearUserPref(PingCentreConstants.TELEMETRY_PREF);
+ Services.prefs.clearUserPref(PingCentreConstants.FHR_UPLOAD_ENABLED_PREF);
+ Services.prefs.clearUserPref(PingCentreConstants.LOGGING_PREF);
+ });
+
+ // On Android, FOG is set up through head.js
+ if (AppConstants.platform != "android") {
+ do_get_profile();
+ Services.fog.initializeFOG();
+ fogInitd = true;
+ }
+});
+
+add_task(function test_enabled() {
+ _setUp();
+ Assert.ok(pingCentre.enabled, "Telemetry should be on");
+});
+
+add_task(function test_disabled_by_pingCentre() {
+ _setUp();
+ Services.prefs.setBoolPref(PingCentreConstants.TELEMETRY_PREF, false);
+
+ Assert.ok(!pingCentre.enabled, "Telemetry should be off");
+});
+
+add_task(function test_disabled_by_FirefoxHealthReport() {
+ _setUp();
+ Services.prefs.setBoolPref(
+ PingCentreConstants.FHR_UPLOAD_ENABLED_PREF,
+ false
+ );
+
+ Assert.ok(!pingCentre.enabled, "Telemetry should be off");
+});
+
+add_task(function test_logging() {
+ _setUp();
+ Assert.ok(pingCentre.logging, "Logging should be on");
+
+ Services.prefs.setBoolPref(PingCentreConstants.LOGGING_PREF, false);
+
+ Assert.ok(!pingCentre.logging, "Logging should be off");
+});
+
+add_task(function test_createExperimentsPayload() {
+ _setUp();
+ const activeExperiments = {
+ exp1: { branch: "foo", enrollmentID: "SOME_RANDON_ID" },
+ exp2: { branch: "bar", type: "PrefStudy" },
+ exp3: {},
+ };
+ sandbox
+ .stub(TelemetryEnvironment, "getActiveExperiments")
+ .returns(activeExperiments);
+ const expected = {
+ exp1: { branch: "foo" },
+ exp2: { branch: "bar" },
+ };
+
+ const experiments = pingCentre._createExperimentsPayload();
+
+ Assert.deepEqual(
+ experiments,
+ expected,
+ "Should create experiments with all the required context"
+ );
+});
+
+add_task(function test_createExperimentsPayload_without_active_experiments() {
+ _setUp();
+ sandbox.stub(TelemetryEnvironment, "getActiveExperiments").returns({});
+ const experiments = pingCentre._createExperimentsPayload({});
+
+ Assert.deepEqual(experiments, {}, "Should send an empty object");
+});
+
+add_task(function test_createStructuredIngestionPing() {
+ _setUp();
+ sandbox
+ .stub(TelemetryEnvironment, "getActiveExperiments")
+ .returns({ exp1: { branch: "foo" } });
+ const ping = pingCentre._createStructuredIngestionPing(FAKE_PING);
+ const expected = {
+ experiments: { exp1: { branch: "foo" } },
+ locale: "en-US",
+ version: AppConstants.MOZ_APP_VERSION,
+ release_channel: UpdateUtils.getUpdateChannel(false),
+ ...FAKE_PING,
+ };
+
+ Assert.deepEqual(ping, expected, "Should create a valid ping");
+});
+
+add_task(function test_sendStructuredIngestionPing_disabled() {
+ _setUp();
+ sandbox.stub(PingCentre, "_sendStandalonePing").resolves();
+ Services.prefs.setBoolPref(PingCentreConstants.TELEMETRY_PREF, false);
+ pingCentre.sendStructuredIngestionPing(FAKE_PING, FAKE_ENDPOINT);
+
+ Assert.ok(PingCentre._sendStandalonePing.notCalled, "Should not be sent");
+});
+
+add_task(async function test_sendStructuredIngestionPing_success() {
+ _setUp();
+ sandbox.stub(PingCentre, "_sendStandalonePing").resolves();
+ await pingCentre.sendStructuredIngestionPing(
+ FAKE_PING,
+ FAKE_ENDPOINT,
+ "messaging-system"
+ );
+
+ Assert.equal(PingCentre._sendStandalonePing.callCount, 1, "Should be sent");
+ Assert.equal(
+ 1,
+ Glean.pingCentre.sendSuccessesByNamespace.messaging_system.testGetValue()
+ );
+
+ // Test an unknown namespace
+ await pingCentre.sendStructuredIngestionPing(
+ FAKE_PING,
+ FAKE_ENDPOINT,
+ "different-system"
+ );
+
+ Assert.equal(PingCentre._sendStandalonePing.callCount, 2, "Should be sent");
+ Assert.equal(
+ 1,
+ Glean.pingCentre.sendSuccessesByNamespace.__other__.testGetValue()
+ );
+});
+
+add_task(async function test_sendStructuredIngestionPing_failure() {
+ _setUp();
+ sandbox.stub(PingCentre, "_sendStandalonePing").rejects();
+ Assert.equal(undefined, Glean.pingCentre.sendFailures.testGetValue());
+ await pingCentre.sendStructuredIngestionPing(
+ FAKE_PING,
+ FAKE_ENDPOINT,
+ "activity-stream"
+ );
+
+ Assert.equal(1, Glean.pingCentre.sendFailures.testGetValue());
+ Assert.equal(
+ 1,
+ Glean.pingCentre.sendFailuresByNamespace.activity_stream.testGetValue()
+ );
+});
diff --git a/browser/modules/test/unit/test_ProfileCounter.js b/browser/modules/test/unit/test_ProfileCounter.js
new file mode 100644
index 0000000000..e8b3179f34
--- /dev/null
+++ b/browser/modules/test/unit/test_ProfileCounter.js
@@ -0,0 +1,239 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+const { BrowserUsageTelemetry } = ChromeUtils.import(
+ "resource:///modules/BrowserUsageTelemetry.jsm"
+);
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const PROFILE_COUNT_SCALAR = "browser.engagement.profile_count";
+// Largest possible uint32_t value represents an error.
+const SCALAR_ERROR_VALUE = 0;
+
+const FILE_OPEN_OPERATION = "open";
+const ERROR_FILE_NOT_FOUND = "NotFoundError";
+const ERROR_ACCESS_DENIED = "NotAllowedError";
+
+// We will redirect I/O to/from the profile counter file to read/write this
+// variable instead. That makes it easier for us to:
+// - avoid interference from any pre-existing file
+// - read and change the values in the file.
+// - clean up changes made to the file
+// We will translate a null value stored here to a File Not Found error.
+var gFakeProfileCounterFile = null;
+// We will use this to check that the profile counter code doesn't try to write
+// to multiple files (since this test will malfunction in that case due to
+// gFakeProfileCounterFile only being setup to accommodate a single file).
+var gProfileCounterFilePath = null;
+
+// Storing a value here lets us test the behavior when we encounter an error
+// reading or writing to the file. A null value means that no error will
+// be simulated (other than possibly a NotFoundError).
+var gNextReadExceptionReason = null;
+var gNextWriteExceptionReason = null;
+
+// Nothing will actually be stored in this directory, so it's not important that
+// it be valid, but the leafname should be unique to this test in order to be
+// sure of preventing name conflicts with the pref:
+// `browser.engagement.profileCounted.${hash}`
+function getDummyUpdateDirectory() {
+ const testName = "test_ProfileCounter";
+ let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ dir.initWithPath(`C:\\foo\\bar\\${testName}`);
+ return dir;
+}
+
+// We aren't going to bother generating anything looking like a real client ID
+// for this. The only real requirements for client ids is that they not repeat
+// and that they be strings. So we'll just return an integer as a string and
+// increment it when we want a new client id.
+var gDummyTelemetryClientId = 0;
+function getDummyTelemetryClientId() {
+ return gDummyTelemetryClientId.toString();
+}
+function setNewDummyTelemetryClientId() {
+ ++gDummyTelemetryClientId;
+}
+
+// Returns null if the (fake) profile count file hasn't been created yet.
+function getProfileCount() {
+ // Strict equality to ensure distinguish properly between a non-existent
+ // file and an empty one.
+ if (gFakeProfileCounterFile === null) {
+ return null;
+ }
+ let saveData = JSON.parse(gFakeProfileCounterFile);
+ return saveData.profileTelemetryIds.length;
+}
+
+// Resets the state to the original state, before the profile count file has
+// even been written.
+// If resetFile is specified as false, this will reset everything except for the
+// file itself. This allows us to sort of pretend that another installation
+// wrote the file.
+function reset(resetFile = true) {
+ if (resetFile) {
+ gFakeProfileCounterFile = null;
+ }
+ gNextReadExceptionReason = null;
+ gNextWriteExceptionReason = null;
+ setNewDummyTelemetryClientId();
+}
+
+function setup() {
+ reset();
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+ // Initialize FOG so we can test the FOG version of profile count
+ Services.fog.initializeFOG();
+ Services.fog.testResetFOG();
+
+ BrowserUsageTelemetry.Policy.readProfileCountFile = async path => {
+ if (!gProfileCounterFilePath) {
+ gProfileCounterFilePath = path;
+ } else {
+ // We've only got one mock-file variable. Make sure we are always
+ // accessing the same file or this will cause problems.
+ Assert.equal(
+ gProfileCounterFilePath,
+ path,
+ "Only one file should be accessed"
+ );
+ }
+ // Strict equality to ensure distinguish properly between null and 0.
+ if (gNextReadExceptionReason !== null) {
+ let ex = new DOMException(FILE_OPEN_OPERATION, gNextReadExceptionReason);
+ gNextReadExceptionReason = null;
+ throw ex;
+ }
+ // Strict equality to ensure distinguish properly between a non-existent
+ // file and an empty one.
+ if (gFakeProfileCounterFile === null) {
+ throw new DOMException(FILE_OPEN_OPERATION, ERROR_FILE_NOT_FOUND);
+ }
+ return gFakeProfileCounterFile;
+ };
+ BrowserUsageTelemetry.Policy.writeProfileCountFile = async (path, data) => {
+ if (!gProfileCounterFilePath) {
+ gProfileCounterFilePath = path;
+ } else {
+ // We've only got one mock-file variable. Make sure we are always
+ // accessing the same file or this will cause problems.
+ Assert.equal(
+ gProfileCounterFilePath,
+ path,
+ "Only one file should be accessed"
+ );
+ }
+ // Strict equality to ensure distinguish properly between null and 0.
+ if (gNextWriteExceptionReason !== null) {
+ let ex = new DOMException(FILE_OPEN_OPERATION, gNextWriteExceptionReason);
+ gNextWriteExceptionReason = null;
+ throw ex;
+ }
+ gFakeProfileCounterFile = data;
+ };
+ BrowserUsageTelemetry.Policy.getUpdateDirectory = getDummyUpdateDirectory;
+ BrowserUsageTelemetry.Policy.getTelemetryClientId = getDummyTelemetryClientId;
+}
+
+// Checks that the number of profiles reported is the number expected. Because
+// of bucketing, the raw count may be different than the reported count.
+function checkSuccess(profilesReported, rawCount = profilesReported) {
+ Assert.equal(rawCount, getProfileCount());
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ PROFILE_COUNT_SCALAR,
+ profilesReported,
+ "The value reported to telemetry should be the expected profile count"
+ );
+ Assert.equal(
+ profilesReported,
+ Glean.browserEngagement.profileCount.testGetValue()
+ );
+}
+
+function checkError() {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ PROFILE_COUNT_SCALAR,
+ SCALAR_ERROR_VALUE,
+ "The value reported to telemetry should be the error value"
+ );
+}
+
+add_task(async function testProfileCounter() {
+ setup();
+
+ info("Testing basic functionality, single install");
+ await BrowserUsageTelemetry.reportProfileCount();
+ checkSuccess(1);
+ await BrowserUsageTelemetry.reportProfileCount();
+ checkSuccess(1);
+
+ // Fake another installation by resetting everything except for the profile
+ // count file.
+ reset(false);
+
+ info("Testing basic functionality, faking a second install");
+ await BrowserUsageTelemetry.reportProfileCount();
+ checkSuccess(2);
+
+ // Check if we properly handle the case where we cannot read from the file
+ // and we have already set its contents. This should report an error.
+ info("Testing read error after successful write");
+ gNextReadExceptionReason = ERROR_ACCESS_DENIED;
+ await BrowserUsageTelemetry.reportProfileCount();
+ checkError();
+
+ reset();
+
+ // A read error should cause an error to be reported, but should also write
+ // to the file in an attempt to fix it. So the next (successful) read should
+ // result in the correct telemetry.
+ info("Testing read error self-correction");
+ gNextReadExceptionReason = ERROR_ACCESS_DENIED;
+ await BrowserUsageTelemetry.reportProfileCount();
+ checkError();
+
+ await BrowserUsageTelemetry.reportProfileCount();
+ checkSuccess(1);
+
+ reset();
+
+ // If the file is malformed. We should report an error and fix it, then report
+ // the correct profile count next time.
+ info("Testing with malformed profile count file");
+ gFakeProfileCounterFile = "<malformed file data>";
+ await BrowserUsageTelemetry.reportProfileCount();
+ checkError();
+
+ await BrowserUsageTelemetry.reportProfileCount();
+ checkSuccess(1);
+
+ reset();
+
+ // If we haven't yet written to the file, a write error should cause an error
+ // to be reported.
+ info("Testing write error before the first write");
+ gNextWriteExceptionReason = ERROR_ACCESS_DENIED;
+ await BrowserUsageTelemetry.reportProfileCount();
+ checkError();
+
+ reset();
+
+ info("Testing bucketing");
+ // Fake 15 installations to drive the raw profile count up to 15.
+ for (let i = 0; i < 15; i++) {
+ reset(false);
+ await BrowserUsageTelemetry.reportProfileCount();
+ }
+ // With bucketing, values from 10-99 should all be reported as 10.
+ checkSuccess(10, 15);
+});
diff --git a/browser/modules/test/unit/test_Sanitizer_interrupted.js b/browser/modules/test/unit/test_Sanitizer_interrupted.js
new file mode 100644
index 0000000000..c8e7130ac0
--- /dev/null
+++ b/browser/modules/test/unit/test_Sanitizer_interrupted.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+do_get_profile();
+
+// Test that interrupted sanitizations are properly tracked.
+
+add_task(async function () {
+ const { Sanitizer } = ChromeUtils.importESModule(
+ "resource:///modules/Sanitizer.sys.mjs"
+ );
+
+ Services.prefs.setBoolPref(Sanitizer.PREF_NEWTAB_SEGREGATION, false);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN);
+ Services.prefs.clearUserPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata");
+ Services.prefs.clearUserPref(Sanitizer.PREF_NEWTAB_SEGREGATION);
+ });
+ Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, true);
+ Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata", true);
+
+ await Sanitizer.onStartup();
+ Assert.ok(Sanitizer.shouldSanitizeOnShutdown, "Should sanitize on shutdown");
+
+ let pendingSanitizations = JSON.parse(
+ Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]")
+ );
+ Assert.equal(
+ pendingSanitizations.length,
+ 1,
+ "Should have 1 pending sanitization"
+ );
+ Assert.equal(
+ pendingSanitizations[0].id,
+ "shutdown",
+ "Should be the shutdown sanitization"
+ );
+ Assert.ok(
+ pendingSanitizations[0].itemsToClear.includes("formdata"),
+ "Pref has been setup"
+ );
+ Assert.ok(
+ !pendingSanitizations[0].options.isShutdown,
+ "Shutdown option is not present"
+ );
+
+ // Check the preference listeners.
+ Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false);
+ pendingSanitizations = JSON.parse(
+ Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]")
+ );
+ Assert.equal(
+ pendingSanitizations.length,
+ 0,
+ "Should not have pending sanitizations"
+ );
+ Assert.ok(
+ !Sanitizer.shouldSanitizeOnShutdown,
+ "Should not sanitize on shutdown"
+ );
+ Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, true);
+ pendingSanitizations = JSON.parse(
+ Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]")
+ );
+ Assert.equal(
+ pendingSanitizations.length,
+ 1,
+ "Should have 1 pending sanitization"
+ );
+ Assert.equal(
+ pendingSanitizations[0].id,
+ "shutdown",
+ "Should be the shutdown sanitization"
+ );
+
+ Assert.ok(
+ pendingSanitizations[0].itemsToClear.includes("formdata"),
+ "Pending sanitizations should include formdata"
+ );
+ Services.prefs.setBoolPref(
+ Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata",
+ false
+ );
+ pendingSanitizations = JSON.parse(
+ Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]")
+ );
+ Assert.equal(
+ pendingSanitizations.length,
+ 1,
+ "Should have 1 pending sanitization"
+ );
+ Assert.ok(
+ !pendingSanitizations[0].itemsToClear.includes("formdata"),
+ "Pending sanitizations should have been updated"
+ );
+
+ // Check a sanitization properly rebuilds the pref.
+ await Sanitizer.sanitize(["formdata"]);
+ pendingSanitizations = JSON.parse(
+ Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]")
+ );
+ Assert.equal(
+ pendingSanitizations.length,
+ 1,
+ "Should have 1 pending sanitization"
+ );
+ Assert.equal(
+ pendingSanitizations[0].id,
+ "shutdown",
+ "Should be the shutdown sanitization"
+ );
+
+ // Startup should run the pending one and setup a new shutdown sanitization.
+ Services.prefs.setBoolPref(
+ Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata",
+ false
+ );
+ await Sanitizer.onStartup();
+ pendingSanitizations = JSON.parse(
+ Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]")
+ );
+ Assert.equal(
+ pendingSanitizations.length,
+ 1,
+ "Should have 1 pending sanitization"
+ );
+ Assert.equal(
+ pendingSanitizations[0].id,
+ "shutdown",
+ "Should be the shutdown sanitization"
+ );
+ Assert.ok(
+ !pendingSanitizations[0].itemsToClear.includes("formdata"),
+ "Pref has been setup"
+ );
+});
diff --git a/browser/modules/test/unit/test_SiteDataManager.js b/browser/modules/test/unit/test_SiteDataManager.js
new file mode 100644
index 0000000000..adceb64ca4
--- /dev/null
+++ b/browser/modules/test/unit/test_SiteDataManager.js
@@ -0,0 +1,277 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+const { SiteDataManager } = ChromeUtils.import(
+ "resource:///modules/SiteDataManager.jsm"
+);
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const EXAMPLE_ORIGIN = "https://www.example.com";
+const EXAMPLE_ORIGIN_2 = "https://example.org";
+const EXAMPLE_ORIGIN_3 = "http://localhost:8000";
+
+let p =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ EXAMPLE_ORIGIN
+ );
+let partitionKey = `(${p.scheme},${p.baseDomain})`;
+let EXAMPLE_ORIGIN_2_PARTITIONED =
+ Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(EXAMPLE_ORIGIN_2),
+ {
+ partitionKey,
+ }
+ ).origin;
+
+add_task(function setup() {
+ do_get_profile();
+});
+
+add_task(async function testGetSites() {
+ SiteDataTestUtils.addToCookies({
+ origin: EXAMPLE_ORIGIN,
+ name: "foo1",
+ value: "bar1",
+ });
+ SiteDataTestUtils.addToCookies({
+ origin: EXAMPLE_ORIGIN,
+ name: "foo2",
+ value: "bar2",
+ });
+
+ // Cookie of EXAMPLE_ORIGIN_2 partitioned under EXAMPLE_ORIGIN.
+ SiteDataTestUtils.addToCookies({
+ origin: EXAMPLE_ORIGIN_2_PARTITIONED,
+ name: "foo3",
+ value: "bar3",
+ });
+ // IndexedDB storage of EXAMPLE_ORIGIN_2 partitioned under EXAMPLE_ORIGIN.
+ await SiteDataTestUtils.addToIndexedDB(EXAMPLE_ORIGIN, 4096);
+ await SiteDataTestUtils.addToIndexedDB(EXAMPLE_ORIGIN_2_PARTITIONED, 4096);
+ SiteDataTestUtils.addToCookies({
+ origin: EXAMPLE_ORIGIN_2,
+ name: "foo",
+ value: "bar",
+ });
+ await SiteDataTestUtils.addToIndexedDB(EXAMPLE_ORIGIN_2, 2048);
+ await SiteDataTestUtils.persist(EXAMPLE_ORIGIN_2);
+
+ await SiteDataManager.updateSites();
+
+ let sites = await SiteDataManager.getSites();
+
+ let site1 = sites.find(site => site.baseDomain == "example.com");
+ let site2 = sites.find(site => site.baseDomain == "example.org");
+
+ Assert.equal(
+ site1.baseDomain,
+ "example.com",
+ "Has the correct base domain for example.com"
+ );
+ // 4096 partitioned + 4096 unpartitioned.
+ Assert.greater(site1.usage, 4096 * 2, "Has correct usage for example.com");
+ Assert.equal(site1.persisted, false, "example.com is not persisted");
+ Assert.equal(
+ site1.cookies.length,
+ 3, // 2 top level, 1 partitioned.
+ "Has correct number of cookies for example.com"
+ );
+ Assert.ok(
+ typeof site1.lastAccessed.getDate == "function",
+ "lastAccessed for example.com is a Date"
+ );
+ Assert.ok(
+ site1.lastAccessed > Date.now() - 60 * 1000,
+ "lastAccessed for example.com happened recently"
+ );
+
+ Assert.equal(
+ site2.baseDomain,
+ "example.org",
+ "Has the correct base domain for example.org"
+ );
+ Assert.greater(site2.usage, 2048, "Has correct usage for example.org");
+ Assert.equal(site2.persisted, true, "example.org is persisted");
+ Assert.equal(
+ site2.cookies.length,
+ 1,
+ "Has correct number of cookies for example.org"
+ );
+ Assert.ok(
+ typeof site2.lastAccessed.getDate == "function",
+ "lastAccessed for example.org is a Date"
+ );
+ Assert.ok(
+ site2.lastAccessed > Date.now() - 60 * 1000,
+ "lastAccessed for example.org happened recently"
+ );
+
+ await SiteDataTestUtils.clear();
+});
+
+add_task(async function testGetTotalUsage() {
+ await SiteDataManager.updateSites();
+ let sites = await SiteDataManager.getSites();
+ Assert.equal(sites.length, 0, "SiteDataManager is empty");
+
+ await SiteDataTestUtils.addToIndexedDB(EXAMPLE_ORIGIN, 4096);
+ await SiteDataTestUtils.addToIndexedDB(EXAMPLE_ORIGIN_2, 2048);
+
+ await SiteDataManager.updateSites();
+
+ let usage = await SiteDataManager.getTotalUsage();
+
+ Assert.greater(usage, 4096 + 2048, "Has the correct total usage.");
+
+ await SiteDataTestUtils.clear();
+});
+
+add_task(async function testRemove() {
+ await SiteDataManager.updateSites();
+
+ let uri = Services.io.newURI(EXAMPLE_ORIGIN);
+ PermissionTestUtils.add(uri, "camera", Services.perms.ALLOW_ACTION);
+
+ SiteDataTestUtils.addToCookies({
+ origin: EXAMPLE_ORIGIN,
+ name: "foo1",
+ value: "bar1",
+ });
+ SiteDataTestUtils.addToCookies({
+ origin: EXAMPLE_ORIGIN,
+ name: "foo2",
+ value: "bar2",
+ });
+ SiteDataTestUtils.addToCookies({
+ origin: EXAMPLE_ORIGIN_2_PARTITIONED,
+ name: "foo3",
+ value: "bar3",
+ });
+ await SiteDataTestUtils.addToIndexedDB(EXAMPLE_ORIGIN, 4096);
+ await SiteDataTestUtils.addToIndexedDB(EXAMPLE_ORIGIN_2_PARTITIONED, 4096);
+ SiteDataTestUtils.addToCookies({
+ origin: EXAMPLE_ORIGIN_2,
+ name: "foo",
+ value: "bar",
+ });
+ await SiteDataTestUtils.addToIndexedDB(EXAMPLE_ORIGIN_2, 2048);
+ await SiteDataTestUtils.persist(EXAMPLE_ORIGIN_2);
+ await SiteDataTestUtils.addToIndexedDB(EXAMPLE_ORIGIN_3, 2048);
+
+ await SiteDataManager.updateSites();
+
+ let sites = await SiteDataManager.getSites();
+
+ Assert.equal(sites.length, 3, "Has three sites.");
+
+ await SiteDataManager.remove("localhost");
+
+ sites = await SiteDataManager.getSites();
+
+ Assert.equal(sites.length, 2, "Has two sites.");
+
+ await SiteDataManager.remove(["www.example.com"]);
+
+ sites = await SiteDataManager.getSites();
+
+ Assert.equal(sites.length, 1, "Has one site.");
+ Assert.equal(
+ sites[0].baseDomain,
+ "example.org",
+ "Has not cleared data for example.org"
+ );
+
+ let usage = await SiteDataTestUtils.getQuotaUsage(EXAMPLE_ORIGIN);
+ Assert.equal(usage, 0, "Has cleared quota usage for example.com");
+
+ let cookies = Services.cookies.countCookiesFromHost("example.com");
+ Assert.equal(cookies, 0, "Has cleared cookies for example.com");
+
+ let perm = PermissionTestUtils.testPermission(uri, "persistent-storage");
+ Assert.equal(
+ perm,
+ Services.perms.UNKNOWN_ACTION,
+ "Cleared the persistent-storage permission."
+ );
+ perm = PermissionTestUtils.testPermission(uri, "camera");
+ Assert.equal(
+ perm,
+ Services.perms.ALLOW_ACTION,
+ "Did not clear other permissions."
+ );
+
+ PermissionTestUtils.remove(uri, "camera");
+});
+
+add_task(async function testRemoveSiteData() {
+ let uri = Services.io.newURI(EXAMPLE_ORIGIN);
+ PermissionTestUtils.add(uri, "camera", Services.perms.ALLOW_ACTION);
+
+ SiteDataTestUtils.addToCookies({
+ origin: EXAMPLE_ORIGIN,
+ name: "foo1",
+ value: "bar1",
+ });
+ SiteDataTestUtils.addToCookies({
+ origin: EXAMPLE_ORIGIN,
+ name: "foo2",
+ value: "bar2",
+ });
+ SiteDataTestUtils.addToCookies({
+ origin: EXAMPLE_ORIGIN_2_PARTITIONED,
+ name: "foo3",
+ value: "bar3",
+ });
+ await SiteDataTestUtils.addToIndexedDB(EXAMPLE_ORIGIN, 4096);
+ await SiteDataTestUtils.addToIndexedDB(EXAMPLE_ORIGIN_2_PARTITIONED, 4096);
+ SiteDataTestUtils.addToCookies({
+ origin: EXAMPLE_ORIGIN_2,
+ name: "foo",
+ value: "bar",
+ });
+ await SiteDataTestUtils.addToIndexedDB(EXAMPLE_ORIGIN_2, 2048);
+ await SiteDataTestUtils.persist(EXAMPLE_ORIGIN_2);
+
+ await SiteDataManager.updateSites();
+
+ let sites = await SiteDataManager.getSites();
+
+ Assert.equal(sites.length, 2, "Has two sites.");
+
+ await SiteDataManager.removeSiteData();
+
+ sites = await SiteDataManager.getSites();
+
+ Assert.equal(sites.length, 0, "Has no sites.");
+
+ let usage = await SiteDataTestUtils.getQuotaUsage(EXAMPLE_ORIGIN);
+ Assert.equal(usage, 0, "Has cleared quota usage for example.com");
+
+ usage = await SiteDataTestUtils.getQuotaUsage(EXAMPLE_ORIGIN_2);
+ Assert.equal(usage, 0, "Has cleared quota usage for example.org");
+
+ let cookies = Services.cookies.countCookiesFromHost("example.org");
+ Assert.equal(cookies, 0, "Has cleared cookies for example.org");
+
+ let perm = PermissionTestUtils.testPermission(uri, "persistent-storage");
+ Assert.equal(
+ perm,
+ Services.perms.UNKNOWN_ACTION,
+ "Cleared the persistent-storage permission."
+ );
+ perm = PermissionTestUtils.testPermission(uri, "camera");
+ Assert.equal(
+ perm,
+ Services.perms.ALLOW_ACTION,
+ "Did not clear other permissions."
+ );
+
+ PermissionTestUtils.remove(uri, "camera");
+});
diff --git a/browser/modules/test/unit/test_SiteDataManagerContainers.js b/browser/modules/test/unit/test_SiteDataManagerContainers.js
new file mode 100644
index 0000000000..d083c41414
--- /dev/null
+++ b/browser/modules/test/unit/test_SiteDataManagerContainers.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+const { SiteDataManager } = ChromeUtils.import(
+ "resource:///modules/SiteDataManager.jsm"
+);
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+
+const EXAMPLE_ORIGIN = "https://www.example.com";
+const EXAMPLE_ORIGIN_2 = "https://example.org";
+
+add_task(function setup() {
+ do_get_profile();
+});
+
+add_task(async function testGetSitesByContainers() {
+ SiteDataTestUtils.addToCookies({
+ origin: EXAMPLE_ORIGIN,
+ name: "foo1",
+ value: "bar1",
+ originAttributes: { userContextId: "1" },
+ });
+ SiteDataTestUtils.addToCookies({
+ origin: EXAMPLE_ORIGIN,
+ name: "foo2",
+ value: "bar2",
+ originAttributes: { userContextId: "2" },
+ });
+ SiteDataTestUtils.addToCookies({
+ origin: EXAMPLE_ORIGIN,
+ name: "foo3",
+ value: "bar3",
+ originAttributes: { userContextId: "2" },
+ });
+ SiteDataTestUtils.addToCookies({
+ origin: EXAMPLE_ORIGIN_2,
+ name: "foo",
+ value: "bar",
+ originAttributes: { userContextId: "3" },
+ });
+
+ await SiteDataTestUtils.addToIndexedDB(
+ EXAMPLE_ORIGIN + "^userContextId=1",
+ 4096
+ );
+ await SiteDataTestUtils.addToIndexedDB(
+ EXAMPLE_ORIGIN_2 + "^userContextId=3",
+ 2048
+ );
+
+ await SiteDataManager.updateSites();
+
+ let sites = await SiteDataManager.getSites();
+
+ let site1Container1 = sites
+ .find(site => site.baseDomain == "example.com")
+ .containersData.get(1);
+
+ let site1Container2 = sites
+ .find(site => site.baseDomain == "example.com")
+ .containersData.get(2);
+
+ let site2Container3 = sites
+ .find(site => site.baseDomain == "example.org")
+ .containersData.get(3);
+
+ Assert.equal(
+ sites.reduce(
+ (accumulator, site) => accumulator + site.containersData.size,
+ 0
+ ),
+ 3,
+ "Has the correct number of sites by containers"
+ );
+
+ Assert.equal(
+ site1Container1.cookiesBlocked,
+ 1,
+ "Has the correct number of cookiesBlocked by containers"
+ );
+
+ Assert.greater(
+ site1Container1.quotaUsage,
+ 4096,
+ "Has correct usage for example.com^userContextId=1"
+ );
+
+ Assert.ok(
+ typeof site1Container1.lastAccessed.getDate == "function",
+ "lastAccessed for example.com^userContextId=1 is a Date"
+ );
+ Assert.ok(
+ site1Container1.lastAccessed > Date.now() - 60 * 1000,
+ "lastAccessed for example.com^userContextId=1 happened recently"
+ );
+
+ Assert.equal(
+ site1Container2.cookiesBlocked,
+ 2,
+ "Has the correct number of cookiesBlocked by containers"
+ );
+
+ Assert.equal(
+ site1Container2.quotaUsage,
+ 0,
+ "Has correct usage for example.org^userContextId=2"
+ );
+
+ Assert.ok(
+ typeof site1Container2.lastAccessed.getDate == "function",
+ "lastAccessed for example.com^userContextId=2 is a Date"
+ );
+
+ Assert.equal(
+ site2Container3.cookiesBlocked,
+ 1,
+ "Has the correct number of cookiesBlocked by containers"
+ );
+
+ Assert.greater(
+ site2Container3.quotaUsage,
+ 2048,
+ "Has correct usage for example.org^userContextId=3"
+ );
+
+ Assert.ok(
+ typeof site2Container3.lastAccessed.getDate == "function",
+ "lastAccessed for example.org^userContextId=3 is a Date"
+ );
+ Assert.ok(
+ site2Container3.lastAccessed > Date.now() - 60 * 1000,
+ "lastAccessed for example.org^userContextId=3 happened recently"
+ );
+
+ await SiteDataTestUtils.clear();
+});
diff --git a/browser/modules/test/unit/test_SitePermissions.js b/browser/modules/test/unit/test_SitePermissions.js
new file mode 100644
index 0000000000..e982cf6e99
--- /dev/null
+++ b/browser/modules/test/unit/test_SitePermissions.js
@@ -0,0 +1,401 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+const { SitePermissions } = ChromeUtils.importESModule(
+ "resource:///modules/SitePermissions.sys.mjs"
+);
+
+const RESIST_FINGERPRINTING_ENABLED = Services.prefs.getBoolPref(
+ "privacy.resistFingerprinting"
+);
+const MIDI_ENABLED = Services.prefs.getBoolPref("dom.webmidi.enabled");
+
+const EXT_PROTOCOL_ENABLED = Services.prefs.getBoolPref(
+ "security.external_protocol_requires_permission"
+);
+
+const SPEAKER_SELECTION_ENABLED = Services.prefs.getBoolPref(
+ "media.setsinkid.enabled"
+);
+
+add_task(async function testPermissionsListing() {
+ let expectedPermissions = [
+ "autoplay-media",
+ "camera",
+ "cookie",
+ "desktop-notification",
+ "focus-tab-by-prompt",
+ "geo",
+ "install",
+ "microphone",
+ "popup",
+ "screen",
+ "shortcuts",
+ "persistent-storage",
+ "storage-access",
+ "xr",
+ "3rdPartyStorage",
+ ];
+ if (RESIST_FINGERPRINTING_ENABLED) {
+ // Canvas permission should be hidden unless privacy.resistFingerprinting
+ // is true.
+ expectedPermissions.push("canvas");
+ }
+ if (MIDI_ENABLED) {
+ // Should remove this checking and add it as default after it is fully pref'd-on.
+ expectedPermissions.push("midi");
+ expectedPermissions.push("midi-sysex");
+ }
+ if (EXT_PROTOCOL_ENABLED) {
+ expectedPermissions.push("open-protocol-handler");
+ }
+ if (SPEAKER_SELECTION_ENABLED) {
+ expectedPermissions.push("speaker");
+ }
+ Assert.deepEqual(
+ SitePermissions.listPermissions().sort(),
+ expectedPermissions.sort(),
+ "Correct list of all permissions"
+ );
+});
+
+add_task(async function testGetAllByPrincipal() {
+ // check that it returns an empty array on an invalid principal
+ // like a principal with an about URI, which doesn't support site permissions
+ let wrongPrincipal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "about:config"
+ );
+ Assert.deepEqual(SitePermissions.getAllByPrincipal(wrongPrincipal), []);
+
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://example.com"
+ );
+ Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), []);
+
+ SitePermissions.setForPrincipal(principal, "camera", SitePermissions.ALLOW);
+ Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), [
+ {
+ id: "camera",
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ ]);
+
+ SitePermissions.setForPrincipal(
+ principal,
+ "microphone",
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_SESSION
+ );
+ SitePermissions.setForPrincipal(
+ principal,
+ "desktop-notification",
+ SitePermissions.BLOCK
+ );
+
+ Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), [
+ {
+ id: "camera",
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ {
+ id: "microphone",
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_SESSION,
+ },
+ {
+ id: "desktop-notification",
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ ]);
+
+ SitePermissions.removeFromPrincipal(principal, "microphone");
+ Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), [
+ {
+ id: "camera",
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ {
+ id: "desktop-notification",
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ ]);
+
+ SitePermissions.removeFromPrincipal(principal, "camera");
+ SitePermissions.removeFromPrincipal(principal, "desktop-notification");
+ Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), []);
+
+ Assert.equal(Services.prefs.getIntPref("permissions.default.shortcuts"), 0);
+ SitePermissions.setForPrincipal(
+ principal,
+ "shortcuts",
+ SitePermissions.BLOCK
+ );
+
+ // Customized preference should have been enabled, but the default should not.
+ Assert.equal(Services.prefs.getIntPref("permissions.default.shortcuts"), 0);
+ Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), [
+ {
+ id: "shortcuts",
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ ]);
+
+ SitePermissions.removeFromPrincipal(principal, "shortcuts");
+ Services.prefs.clearUserPref("permissions.default.shortcuts");
+});
+
+add_task(async function testGetAvailableStates() {
+ Assert.deepEqual(SitePermissions.getAvailableStates("camera"), [
+ SitePermissions.UNKNOWN,
+ SitePermissions.ALLOW,
+ SitePermissions.BLOCK,
+ ]);
+
+ // Test available states with a default permission set.
+ Services.prefs.setIntPref(
+ "permissions.default.camera",
+ SitePermissions.ALLOW
+ );
+ Assert.deepEqual(SitePermissions.getAvailableStates("camera"), [
+ SitePermissions.PROMPT,
+ SitePermissions.ALLOW,
+ SitePermissions.BLOCK,
+ ]);
+ Services.prefs.clearUserPref("permissions.default.camera");
+
+ Assert.deepEqual(SitePermissions.getAvailableStates("cookie"), [
+ SitePermissions.ALLOW,
+ SitePermissions.ALLOW_COOKIES_FOR_SESSION,
+ SitePermissions.BLOCK,
+ ]);
+
+ Assert.deepEqual(SitePermissions.getAvailableStates("popup"), [
+ SitePermissions.ALLOW,
+ SitePermissions.BLOCK,
+ ]);
+});
+
+add_task(async function testExactHostMatch() {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://example.com"
+ );
+ let subPrincipal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://test1.example.com"
+ );
+
+ let exactHostMatched = [
+ "autoplay-media",
+ "desktop-notification",
+ "focus-tab-by-prompt",
+ "camera",
+ "microphone",
+ "screen",
+ "geo",
+ "xr",
+ "persistent-storage",
+ ];
+ if (RESIST_FINGERPRINTING_ENABLED) {
+ // Canvas permission should be hidden unless privacy.resistFingerprinting
+ // is true.
+ exactHostMatched.push("canvas");
+ }
+ if (MIDI_ENABLED) {
+ // WebMIDI is only pref'd on in nightly.
+ // Should remove this checking and add it as default after it is fully pref-on.
+ exactHostMatched.push("midi");
+ exactHostMatched.push("midi-sysex");
+ }
+ if (EXT_PROTOCOL_ENABLED) {
+ exactHostMatched.push("open-protocol-handler");
+ }
+ if (SPEAKER_SELECTION_ENABLED) {
+ exactHostMatched.push("speaker");
+ }
+ let nonExactHostMatched = [
+ "cookie",
+ "popup",
+ "install",
+ "shortcuts",
+ "storage-access",
+ "3rdPartyStorage",
+ ];
+
+ let permissions = SitePermissions.listPermissions();
+ for (let permission of permissions) {
+ SitePermissions.setForPrincipal(
+ principal,
+ permission,
+ SitePermissions.ALLOW
+ );
+
+ if (exactHostMatched.includes(permission)) {
+ // Check that the sub-origin does not inherit the permission from its parent.
+ Assert.equal(
+ SitePermissions.getForPrincipal(subPrincipal, permission).state,
+ SitePermissions.getDefault(permission),
+ `${permission} should exact-host match`
+ );
+ } else if (nonExactHostMatched.includes(permission)) {
+ // Check that the sub-origin does inherit the permission from its parent.
+ Assert.equal(
+ SitePermissions.getForPrincipal(subPrincipal, permission).state,
+ SitePermissions.ALLOW,
+ `${permission} should not exact-host match`
+ );
+ } else {
+ Assert.ok(
+ false,
+ `Found an unknown permission ${permission} in exact host match test.` +
+ "Please add new permissions from SitePermissions.sys.mjs to this test."
+ );
+ }
+
+ // Check that the permission can be made specific to the sub-origin.
+ SitePermissions.setForPrincipal(
+ subPrincipal,
+ permission,
+ SitePermissions.PROMPT
+ );
+ Assert.equal(
+ SitePermissions.getForPrincipal(subPrincipal, permission).state,
+ SitePermissions.PROMPT
+ );
+ Assert.equal(
+ SitePermissions.getForPrincipal(principal, permission).state,
+ SitePermissions.ALLOW
+ );
+
+ SitePermissions.removeFromPrincipal(subPrincipal, permission);
+ SitePermissions.removeFromPrincipal(principal, permission);
+ }
+});
+
+add_task(async function testDefaultPrefs() {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://example.com"
+ );
+
+ // Check that without a pref the default return value is UNKNOWN.
+ Assert.deepEqual(SitePermissions.getForPrincipal(principal, "camera"), {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ });
+
+ // Check that the default return value changed after setting the pref.
+ Services.prefs.setIntPref(
+ "permissions.default.camera",
+ SitePermissions.BLOCK
+ );
+ Assert.deepEqual(SitePermissions.getForPrincipal(principal, "camera"), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ });
+
+ // Check that other permissions still return UNKNOWN.
+ Assert.deepEqual(SitePermissions.getForPrincipal(principal, "microphone"), {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ });
+
+ // Check that the default return value changed after changing the pref.
+ Services.prefs.setIntPref(
+ "permissions.default.camera",
+ SitePermissions.ALLOW
+ );
+ Assert.deepEqual(SitePermissions.getForPrincipal(principal, "camera"), {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ });
+
+ // Check that the preference is ignored if there is a value.
+ SitePermissions.setForPrincipal(principal, "camera", SitePermissions.BLOCK);
+ Assert.deepEqual(SitePermissions.getForPrincipal(principal, "camera"), {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ });
+
+ // The preference should be honored again, after resetting the permissions.
+ SitePermissions.removeFromPrincipal(principal, "camera");
+ Assert.deepEqual(SitePermissions.getForPrincipal(principal, "camera"), {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ });
+
+ // Should be UNKNOWN after clearing the pref.
+ Services.prefs.clearUserPref("permissions.default.camera");
+ Assert.deepEqual(SitePermissions.getForPrincipal(principal, "camera"), {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ });
+});
+
+add_task(async function testCanvasPermission() {
+ let resistFingerprinting = Services.prefs.getBoolPref(
+ "privacy.resistFingerprinting",
+ false
+ );
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://example.com"
+ );
+
+ SitePermissions.setForPrincipal(principal, "canvas", SitePermissions.ALLOW);
+
+ // Canvas permission is hidden when privacy.resistFingerprinting is false.
+ Services.prefs.setBoolPref("privacy.resistFingerprinting", false);
+ Assert.equal(SitePermissions.listPermissions().indexOf("canvas"), -1);
+ Assert.equal(
+ SitePermissions.getAllByPrincipal(principal).filter(
+ permission => permission.id === "canvas"
+ ).length,
+ 0
+ );
+
+ // Canvas permission is visible when privacy.resistFingerprinting is true.
+ Services.prefs.setBoolPref("privacy.resistFingerprinting", true);
+ Assert.notEqual(SitePermissions.listPermissions().indexOf("canvas"), -1);
+ Assert.notEqual(
+ SitePermissions.getAllByPrincipal(principal).filter(
+ permission => permission.id === "canvas"
+ ).length,
+ 0
+ );
+
+ SitePermissions.removeFromPrincipal(principal, "canvas");
+ Services.prefs.setBoolPref(
+ "privacy.resistFingerprinting",
+ resistFingerprinting
+ );
+});
+
+add_task(async function testFilePermissions() {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "file:///example.js"
+ );
+ Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), []);
+
+ SitePermissions.setForPrincipal(principal, "camera", SitePermissions.ALLOW);
+ Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), [
+ {
+ id: "camera",
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ ]);
+ SitePermissions.removeFromPrincipal(principal, "camera");
+ Assert.deepEqual(SitePermissions.getAllByPrincipal(principal), []);
+});
diff --git a/browser/modules/test/unit/test_SitePermissions_temporary.js b/browser/modules/test/unit/test_SitePermissions_temporary.js
new file mode 100644
index 0000000000..a91b1b8bd8
--- /dev/null
+++ b/browser/modules/test/unit/test_SitePermissions_temporary.js
@@ -0,0 +1,710 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+const { SitePermissions } = ChromeUtils.importESModule(
+ "resource:///modules/SitePermissions.sys.mjs"
+);
+
+const TemporaryPermissions = SitePermissions._temporaryPermissions;
+
+const PERM_A = "foo";
+const PERM_B = "bar";
+const PERM_C = "foobar";
+
+const BROWSER_A = createDummyBrowser("https://example.com/foo");
+const BROWSER_B = createDummyBrowser("https://example.org/foo");
+
+const EXPIRY_MS_A = 1000000;
+const EXPIRY_MS_B = 1000001;
+
+function createDummyBrowser(spec) {
+ let uri = Services.io.newURI(spec);
+ return {
+ currentURI: uri,
+ contentPrincipal: Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ ),
+ dispatchEvent: () => {},
+ ownerGlobal: {
+ CustomEvent: class CustomEvent {},
+ },
+ };
+}
+
+function navigateDummyBrowser(browser, uri) {
+ // Callers may pass in either uri strings or nsIURI objects.
+ if (typeof uri == "string") {
+ uri = Services.io.newURI(uri);
+ }
+ browser.currentURI = uri;
+ browser.contentPrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ browser.currentURI,
+ {}
+ );
+}
+
+/**
+ * Tests that temporary permissions with different block states are stored
+ * (set, overwrite, delete) correctly.
+ */
+add_task(async function testAllowBlock() {
+ // Set two temporary permissions on the same browser.
+ SitePermissions.setForPrincipal(
+ null,
+ PERM_A,
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_TEMPORARY,
+ BROWSER_A,
+ EXPIRY_MS_A
+ );
+
+ SitePermissions.setForPrincipal(
+ null,
+ PERM_B,
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_TEMPORARY,
+ BROWSER_A,
+ EXPIRY_MS_A
+ );
+
+ // Test that the permissions have been set correctly.
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, PERM_A, BROWSER_A),
+ {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ },
+ "SitePermissions returns expected permission state for perm A."
+ );
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, PERM_B, BROWSER_A),
+ {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ },
+ "SitePermissions returns expected permission state for perm B."
+ );
+
+ Assert.deepEqual(
+ TemporaryPermissions.get(BROWSER_A, PERM_A),
+ {
+ id: PERM_A,
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ },
+ "TemporaryPermissions returns expected permission state for perm A."
+ );
+
+ Assert.deepEqual(
+ TemporaryPermissions.get(BROWSER_A, PERM_B),
+ {
+ id: PERM_B,
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ },
+ "TemporaryPermissions returns expected permission state for perm B."
+ );
+
+ // Test internal data structure of TemporaryPermissions.
+ let entry = TemporaryPermissions._stateByBrowser.get(BROWSER_A);
+ ok(entry, "Should have an entry for browser A");
+ ok(
+ !TemporaryPermissions._stateByBrowser.has(BROWSER_B),
+ "Should have no entry for browser B"
+ );
+
+ let { browser, uriToPerm } = entry;
+ Assert.equal(
+ browser?.get(),
+ BROWSER_A,
+ "Entry should have a weak reference to the browser."
+ );
+
+ ok(uriToPerm, "Entry should have uriToPerm object.");
+ Assert.equal(Object.keys(uriToPerm).length, 2, "uriToPerm has 2 entries.");
+
+ let permissionsA = uriToPerm[BROWSER_A.contentPrincipal.origin];
+ let permissionsB =
+ uriToPerm[Services.eTLD.getBaseDomain(BROWSER_A.currentURI)];
+
+ ok(permissionsA, "Allow should be keyed under origin");
+ ok(permissionsB, "Block should be keyed under baseDomain");
+
+ let permissionA = permissionsA[PERM_A];
+ let permissionB = permissionsB[PERM_B];
+
+ Assert.equal(
+ permissionA.state,
+ SitePermissions.ALLOW,
+ "Should have correct state"
+ );
+ let expireTimeoutA = permissionA.expireTimeout;
+ Assert.ok(
+ Number.isInteger(expireTimeoutA),
+ "Should have valid expire timeout"
+ );
+
+ Assert.equal(
+ permissionB.state,
+ SitePermissions.BLOCK,
+ "Should have correct state"
+ );
+ let expireTimeoutB = permissionB.expireTimeout;
+ Assert.ok(
+ Number.isInteger(expireTimeoutB),
+ "Should have valid expire timeout"
+ );
+
+ // Overwrite permission A.
+ SitePermissions.setForPrincipal(
+ null,
+ PERM_A,
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_TEMPORARY,
+ BROWSER_A,
+ EXPIRY_MS_B
+ );
+
+ Assert.ok(
+ permissionsA[PERM_A].expireTimeout != expireTimeoutA,
+ "Overwritten permission A should have new timer"
+ );
+
+ // Overwrite permission B - this time with a non-block state which means it
+ // should be keyed by origin now.
+ SitePermissions.setForPrincipal(
+ null,
+ PERM_B,
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_TEMPORARY,
+ BROWSER_A,
+ EXPIRY_MS_A
+ );
+
+ let baseDomainEntry =
+ uriToPerm[Services.eTLD.getBaseDomain(BROWSER_A.currentURI)];
+ Assert.ok(
+ !baseDomainEntry || !baseDomainEntry[PERM_B],
+ "Should not longer have baseDomain permission entry"
+ );
+
+ permissionsB = uriToPerm[BROWSER_A.contentPrincipal.origin];
+ permissionB = permissionsB[PERM_B];
+ Assert.ok(
+ permissionsB && permissionB,
+ "Overwritten permission should be keyed under origin"
+ );
+ Assert.equal(
+ permissionB.state,
+ SitePermissions.ALLOW,
+ "Should have correct updated state"
+ );
+ Assert.ok(
+ permissionB.expireTimeout != expireTimeoutB,
+ "Overwritten permission B should have new timer"
+ );
+
+ // Remove permissions
+ SitePermissions.removeFromPrincipal(null, PERM_A, BROWSER_A);
+ SitePermissions.removeFromPrincipal(null, PERM_B, BROWSER_A);
+
+ // Test that permissions have been removed correctly
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, PERM_A, BROWSER_A),
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "SitePermissions returns UNKNOWN state for A."
+ );
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, PERM_B, BROWSER_A),
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "SitePermissions returns UNKNOWN state for B."
+ );
+
+ Assert.equal(
+ TemporaryPermissions.get(BROWSER_A, PERM_A),
+ null,
+ "TemporaryPermissions returns null for perm A."
+ );
+
+ Assert.equal(
+ TemporaryPermissions.get(BROWSER_A, PERM_B),
+ null,
+ "TemporaryPermissions returns null for perm B."
+ );
+});
+
+/**
+ * Tests TemporaryPermissions#getAll.
+ */
+add_task(async function testGetAll() {
+ SitePermissions.setForPrincipal(
+ null,
+ PERM_A,
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_TEMPORARY,
+ BROWSER_A,
+ EXPIRY_MS_A
+ );
+ SitePermissions.setForPrincipal(
+ null,
+ PERM_B,
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_TEMPORARY,
+ BROWSER_B,
+ EXPIRY_MS_A
+ );
+ SitePermissions.setForPrincipal(
+ null,
+ PERM_C,
+ SitePermissions.PROMPT,
+ SitePermissions.SCOPE_TEMPORARY,
+ BROWSER_B,
+ EXPIRY_MS_A
+ );
+
+ Assert.deepEqual(TemporaryPermissions.getAll(BROWSER_A), [
+ {
+ id: PERM_A,
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ },
+ ]);
+
+ let permsBrowserB = TemporaryPermissions.getAll(BROWSER_B);
+ Assert.equal(
+ permsBrowserB.length,
+ 2,
+ "There should be 2 permissions set for BROWSER_B"
+ );
+
+ let permB;
+ let permC;
+
+ if (permsBrowserB[0].id == PERM_B) {
+ permB = permsBrowserB[0];
+ permC = permsBrowserB[1];
+ } else {
+ permB = permsBrowserB[1];
+ permC = permsBrowserB[0];
+ }
+
+ Assert.deepEqual(permB, {
+ id: PERM_B,
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+ Assert.deepEqual(permC, {
+ id: PERM_C,
+ state: SitePermissions.PROMPT,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ });
+});
+
+/**
+ * Tests SitePermissions#clearTemporaryBlockPermissions and
+ * TemporaryPermissions#clear.
+ */
+add_task(async function testClear() {
+ SitePermissions.setForPrincipal(
+ null,
+ PERM_A,
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_TEMPORARY,
+ BROWSER_A,
+ EXPIRY_MS_A
+ );
+ SitePermissions.setForPrincipal(
+ null,
+ PERM_B,
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_TEMPORARY,
+ BROWSER_A,
+ EXPIRY_MS_A
+ );
+ SitePermissions.setForPrincipal(
+ null,
+ PERM_C,
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_TEMPORARY,
+ BROWSER_B,
+ EXPIRY_MS_A
+ );
+
+ let stateByBrowser = SitePermissions._temporaryPermissions._stateByBrowser;
+
+ Assert.ok(stateByBrowser.has(BROWSER_A), "Browser map should have BROWSER_A");
+ Assert.ok(stateByBrowser.has(BROWSER_B), "Browser map should have BROWSER_B");
+
+ SitePermissions.clearTemporaryBlockPermissions(BROWSER_A);
+
+ // We only clear block permissions, so we should still see PERM_A.
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, PERM_A, BROWSER_A),
+ {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ },
+ "SitePermissions returns ALLOW state for PERM_A."
+ );
+ // We don't clear BROWSER_B so it should still be there.
+ Assert.ok(stateByBrowser.has(BROWSER_B), "Should still have BROWSER_B.");
+
+ // Now clear allow permissions for A explicitly.
+ SitePermissions._temporaryPermissions.clear(BROWSER_A, SitePermissions.ALLOW);
+
+ Assert.ok(!stateByBrowser.has(BROWSER_A), "Should no longer have BROWSER_A.");
+ let browser = stateByBrowser.get(BROWSER_B);
+ Assert.ok(browser, "Should still have BROWSER_B");
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, PERM_A, BROWSER_A),
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "SitePermissions returns UNKNOWN state for PERM_A."
+ );
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, PERM_B, BROWSER_A),
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "SitePermissions returns UNKNOWN state for PERM_B."
+ );
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, PERM_C, BROWSER_B),
+ {
+ state: SitePermissions.BLOCK,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ },
+ "SitePermissions returns BLOCK state for PERM_C."
+ );
+
+ SitePermissions._temporaryPermissions.clear(BROWSER_B);
+
+ Assert.ok(!stateByBrowser.has(BROWSER_B), "Should no longer have BROWSER_B.");
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, PERM_C, BROWSER_B),
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "SitePermissions returns UNKNOWN state for PERM_C."
+ );
+});
+
+/**
+ * Tests that the temporary permissions setter calls the callback on permission
+ * expire with the associated browser.
+ */
+add_task(async function testCallbackOnExpiry() {
+ let promiseExpireA = new Promise(resolve => {
+ TemporaryPermissions.set(
+ BROWSER_A,
+ PERM_A,
+ SitePermissions.BLOCK,
+ 100,
+ undefined,
+ resolve
+ );
+ });
+ let promiseExpireB = new Promise(resolve => {
+ TemporaryPermissions.set(
+ BROWSER_B,
+ PERM_A,
+ SitePermissions.BLOCK,
+ 100,
+ BROWSER_B.contentPrincipal,
+ resolve
+ );
+ });
+
+ let [browserA, browserB] = await Promise.all([
+ promiseExpireA,
+ promiseExpireB,
+ ]);
+ Assert.equal(
+ browserA,
+ BROWSER_A,
+ "Should get callback with browser on expiry for A"
+ );
+ Assert.equal(
+ browserB,
+ BROWSER_B,
+ "Should get callback with browser on expiry for B"
+ );
+});
+
+/**
+ * Tests that the temporary permissions setter calls the callback on permission
+ * expire with the associated browser if the browser associated browser has
+ * changed after setting the permission.
+ */
+add_task(async function testCallbackOnExpiryUpdatedBrowser() {
+ let promiseExpire = new Promise(resolve => {
+ TemporaryPermissions.set(
+ BROWSER_A,
+ PERM_A,
+ SitePermissions.BLOCK,
+ 200,
+ undefined,
+ resolve
+ );
+ });
+
+ TemporaryPermissions.copy(BROWSER_A, BROWSER_B);
+
+ let browser = await promiseExpire;
+ Assert.equal(
+ browser,
+ BROWSER_B,
+ "Should get callback with updated browser on expiry."
+ );
+});
+
+/**
+ * Tests that the permission setter throws an exception if an invalid expiry
+ * time is passed.
+ */
+add_task(async function testInvalidExpiryTime() {
+ let expectedError = /expireTime must be a positive integer/;
+ Assert.throws(() => {
+ SitePermissions.setForPrincipal(
+ null,
+ PERM_A,
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_TEMPORARY,
+ BROWSER_A,
+ null
+ );
+ }, expectedError);
+ Assert.throws(() => {
+ SitePermissions.setForPrincipal(
+ null,
+ PERM_A,
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_TEMPORARY,
+ BROWSER_A,
+ 0
+ );
+ }, expectedError);
+ Assert.throws(() => {
+ SitePermissions.setForPrincipal(
+ null,
+ PERM_A,
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_TEMPORARY,
+ BROWSER_A,
+ -100
+ );
+ }, expectedError);
+});
+
+/**
+ * Tests that we block by base domain but allow by origin.
+ */
+add_task(async function testTemporaryPermissionScope() {
+ let states = {
+ strict: {
+ same: [
+ "https://example.com",
+ "https://example.com/sub/path",
+ "https://example.com:443",
+ "https://name:password@example.com",
+ ],
+ different: [
+ "https://example.com",
+ "https://test1.example.com",
+ "http://example.com",
+ "http://example.org",
+ "file:///tmp/localPageA.html",
+ "file:///tmp/localPageB.html",
+ ],
+ },
+ nonStrict: {
+ same: [
+ "https://example.com",
+ "https://example.com/sub/path",
+ "https://example.com:443",
+ "https://test1.example.com",
+ "http://test2.test1.example.com",
+ "https://name:password@example.com",
+ "http://example.com",
+ ],
+ different: [
+ "https://example.com",
+ "https://example.org",
+ "http://example.net",
+ ],
+ },
+ };
+
+ for (let state of [SitePermissions.BLOCK, SitePermissions.ALLOW]) {
+ let matchStrict = state != SitePermissions.BLOCK;
+
+ let lists = matchStrict ? states.strict : states.nonStrict;
+
+ Object.entries(lists).forEach(([type, list]) => {
+ let expectSet = type == "same";
+
+ for (let uri of list) {
+ let browser = createDummyBrowser(uri);
+ SitePermissions.setForPrincipal(
+ null,
+ PERM_A,
+ state,
+ SitePermissions.SCOPE_TEMPORARY,
+ browser,
+ EXPIRY_MS_A
+ );
+
+ ok(true, "origin:" + browser.contentPrincipal.origin);
+
+ for (let otherUri of list) {
+ if (uri == otherUri) {
+ continue;
+ }
+ navigateDummyBrowser(browser, otherUri);
+ ok(true, "new origin:" + browser.contentPrincipal.origin);
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, PERM_A, browser),
+ {
+ state: expectSet ? state : SitePermissions.UNKNOWN,
+ scope: expectSet
+ ? SitePermissions.SCOPE_TEMPORARY
+ : SitePermissions.SCOPE_PERSISTENT,
+ },
+ `${
+ state == SitePermissions.BLOCK ? "Block" : "Allow"
+ } Permission originally set for ${uri} should ${
+ expectSet ? "not" : "also"
+ } be set for ${otherUri}.`
+ );
+ }
+
+ SitePermissions._temporaryPermissions.clear(browser);
+ }
+ });
+ }
+});
+
+/**
+ * Tests that we can override the principal to use for keying temporary
+ * permissions.
+ */
+add_task(async function testOverrideBrowserURI() {
+ let testBrowser = createDummyBrowser("https://old.example.com/foo");
+ let overrideURI = Services.io.newURI("https://test.example.org/test/path");
+ SitePermissions.setForPrincipal(
+ Services.scriptSecurityManager.createContentPrincipal(overrideURI, {}),
+ PERM_A,
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_TEMPORARY,
+ testBrowser,
+ EXPIRY_MS_A
+ );
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, PERM_A, testBrowser),
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "Permission should not be set for old URI."
+ );
+
+ // "Navigate" to new URI
+ navigateDummyBrowser(testBrowser, overrideURI);
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, PERM_A, testBrowser),
+ {
+ state: SitePermissions.ALLOW,
+ scope: SitePermissions.SCOPE_TEMPORARY,
+ },
+ "Permission should be set for new URI."
+ );
+
+ SitePermissions._temporaryPermissions.clear(testBrowser);
+});
+
+/**
+ * Tests that TemporaryPermissions does not throw for incompatible URI or
+ * browser.currentURI.
+ */
+add_task(async function testPermissionUnsupportedScheme() {
+ let aboutURI = Services.io.newURI("about:blank");
+
+ // Incompatible override URI should not throw or store any permissions.
+ SitePermissions.setForPrincipal(
+ Services.scriptSecurityManager.createContentPrincipal(aboutURI, {}),
+ PERM_A,
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_TEMPORARY,
+ BROWSER_A,
+ EXPIRY_MS_B
+ );
+ Assert.ok(
+ SitePermissions._temporaryPermissions._stateByBrowser.has(BROWSER_A),
+ "Should not have stored permission for unsupported URI scheme."
+ );
+
+ let browser = createDummyBrowser("https://example.com/");
+ // Set a permission so we get an entry in the browser map.
+ SitePermissions.setForPrincipal(
+ null,
+ PERM_B,
+ SitePermissions.BLOCK,
+ SitePermissions.SCOPE_TEMPORARY,
+ browser
+ );
+
+ // Change browser URI to about:blank.
+ navigateDummyBrowser(browser, aboutURI);
+
+ // Setting permission for browser with unsupported URI should not throw.
+ SitePermissions.setForPrincipal(
+ null,
+ PERM_A,
+ SitePermissions.ALLOW,
+ SitePermissions.SCOPE_TEMPORARY,
+ browser
+ );
+ Assert.ok(true, "Set should not throw for unsupported URI");
+
+ SitePermissions.removeFromPrincipal(null, PERM_A, browser);
+ Assert.ok(true, "Remove should not throw for unsupported URI");
+
+ Assert.deepEqual(
+ SitePermissions.getForPrincipal(null, PERM_A, browser),
+ {
+ state: SitePermissions.UNKNOWN,
+ scope: SitePermissions.SCOPE_PERSISTENT,
+ },
+ "Should return no permission set for unsupported URI."
+ );
+ Assert.ok(true, "Get should not throw for unsupported URI");
+
+ // getAll should not throw, but return empty permissions array.
+ let permissions = SitePermissions.getAllForBrowser(browser);
+ Assert.ok(
+ Array.isArray(permissions) && !permissions.length,
+ "Should return empty array for browser on about:blank"
+ );
+
+ SitePermissions._temporaryPermissions.clear(browser);
+});
diff --git a/browser/modules/test/unit/test_TabUnloader.js b/browser/modules/test/unit/test_TabUnloader.js
new file mode 100644
index 0000000000..2177fe14e2
--- /dev/null
+++ b/browser/modules/test/unit/test_TabUnloader.js
@@ -0,0 +1,449 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+const { TabUnloader } = ChromeUtils.import(
+ "resource:///modules/TabUnloader.jsm"
+);
+
+let TestTabUnloaderMethods = {
+ isNonDiscardable(tab, weight) {
+ return /\bselected\b/.test(tab.keywords) ? weight : 0;
+ },
+
+ isParentProcess(tab, weight) {
+ return /\bparent\b/.test(tab.keywords) ? weight : 0;
+ },
+
+ isPinned(tab, weight) {
+ return /\bpinned\b/.test(tab.keywords) ? weight : 0;
+ },
+
+ isLoading(tab, weight) {
+ return /\bloading\b/.test(tab.keywords) ? weight : 0;
+ },
+
+ usingPictureInPicture(tab, weight) {
+ return /\bpictureinpicture\b/.test(tab.keywords) ? weight : 0;
+ },
+
+ playingMedia(tab, weight) {
+ return /\bmedia\b/.test(tab.keywords) ? weight : 0;
+ },
+
+ usingWebRTC(tab, weight) {
+ return /\bwebrtc\b/.test(tab.keywords) ? weight : 0;
+ },
+
+ isPrivate(tab, weight) {
+ return /\bprivate\b/.test(tab.keywords) ? weight : 0;
+ },
+
+ getMinTabCount() {
+ // Use a low number for testing.
+ return 3;
+ },
+
+ getNow() {
+ return 100;
+ },
+
+ *iterateProcesses(tab) {
+ for (let process of tab.process.split(",")) {
+ yield Number(process);
+ }
+ },
+
+ async calculateMemoryUsage(processMap, tabs) {
+ let memory = tabs[0].memory;
+ for (let pid of processMap.keys()) {
+ processMap.get(pid).memory = memory ? memory[pid - 1] : 1;
+ }
+ },
+};
+
+let unloadTests = [
+ // Each item in the array represents one test. The test is a subarray
+ // containing an element per tab. This is a string of keywords that
+ // identify which criteria apply. The first part of the string may contain
+ // a number that represents the last visit time, where higher numbers
+ // are later. The last element in the subarray is special and identifies
+ // the expected order of the tabs sorted by weight. The first tab in
+ // this list is the one that is expected to selected to be discarded.
+ { tabs: ["1 selected", "2", "3"], result: "1,2,0" },
+ { tabs: ["1", "2 selected", "3"], result: "0,2,1" },
+ { tabs: ["1 selected", "2", "3"], process: ["1", "2", "3"], result: "1,2,0" },
+ {
+ tabs: ["1 selected", "2 selected", "3 selected"],
+ process: ["1", "2", "3"],
+ result: "0,1,2",
+ },
+ {
+ tabs: ["1 selected", "2", "3"],
+ process: ["1,2,3", "2", "3"],
+ result: "1,2,0",
+ },
+ {
+ tabs: ["9", "8", "6", "5 selected", "2", "3", "4", "1"],
+ result: "7,4,5,6,2,1,0,3",
+ },
+ {
+ tabs: ["9", "8 pinned", "6", "5 selected", "2", "3 pinned", "4", "1"],
+ result: "7,4,6,2,0,5,1,3",
+ },
+ {
+ tabs: [
+ "9",
+ "8 pinned",
+ "6",
+ "5 selected pinned",
+ "2",
+ "3 pinned",
+ "4",
+ "1",
+ ],
+ result: "7,4,6,2,0,5,1,3",
+ },
+ {
+ tabs: [
+ "9",
+ "8 pinned",
+ "6",
+ "5 selected pinned",
+ "2",
+ "3 selected pinned",
+ "4",
+ "1",
+ ],
+ result: "7,4,6,2,0,1,5,3",
+ },
+ {
+ tabs: ["1", "2 selected", "3", "4 media", "5", "6"],
+ result: "0,2,4,5,1,3",
+ },
+ {
+ tabs: ["1 media", "2 selected media", "3", "4 media", "5", "6"],
+ result: "2,4,5,0,3,1",
+ },
+ {
+ tabs: ["1 media", "2 media pinned", "3", "4 media", "5 pinned", "6"],
+ result: "2,5,4,0,3,1",
+ },
+ {
+ tabs: [
+ "1 media",
+ "2 media pinned",
+ "3",
+ "4 media",
+ "5 media pinned",
+ "6 selected",
+ ],
+ result: "2,0,3,5,1,4",
+ },
+ {
+ tabs: [
+ "10 selected",
+ "20 private",
+ "30 webrtc",
+ "40 pictureinpicture",
+ "50 loading pinned",
+ "60",
+ ],
+ result: "5,4,0,1,2,3",
+ },
+ {
+ // Since TestTabUnloaderMethods.getNow() returns 100 and the test
+ // passes minInactiveDuration = 0 to TabUnloader.getSortedTabs(),
+ // tab 200 and 300 are excluded from the result.
+ tabs: ["300", "10", "50", "100", "200"],
+ result: "1,2,3",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5", "6"],
+ process: ["1", "2", "1", "1", "1", "1"],
+ result: "1,0,2,3,4,5",
+ },
+ {
+ tabs: ["1", "2 selected", "3", "4", "5", "6"],
+ process: ["1", "2", "1", "1", "1", "1"],
+ result: "0,2,3,4,5,1",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5", "6"],
+ process: ["1", "2", "2", "1", "1", "1"],
+ result: "0,1,2,3,4,5",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5", "6"],
+ process: ["1", "2", "3", "1", "1", "1"],
+ result: "1,0,2,3,4,5",
+ },
+ {
+ tabs: ["1", "2 media", "3", "4", "5", "6"],
+ process: ["1", "2", "3", "1", "1", "1"],
+ result: "2,0,3,4,5,1",
+ },
+ {
+ tabs: ["1", "2 media", "3", "4", "5", "6"],
+ process: ["1", "2", "3", "1", "1,2,3", "1"],
+ result: "0,2,3,4,5,1",
+ },
+ {
+ tabs: ["1", "2 media", "3", "4", "5", "6"],
+ process: ["1", "2", "3", "1", "1,4,5", "1"],
+ result: "2,0,3,4,5,1",
+ },
+ {
+ tabs: ["1", "2 media", "3 media", "4", "5 media", "6"],
+ process: ["1", "2", "3", "1", "1,4,5", "1"],
+ result: "0,3,5,1,2,4",
+ },
+ {
+ tabs: ["1", "2 media", "3 media", "4", "5 media", "6"],
+ process: ["1", "1", "3", "1", "1,4,5", "1"],
+ result: "0,3,5,1,2,4",
+ },
+ {
+ tabs: ["1", "2 media", "3 media", "4", "5 media", "6"],
+ process: ["1", "2", "3", "4", "1,4,5", "5"],
+ result: "0,3,5,1,2,4",
+ },
+ {
+ tabs: ["1", "2 media", "3 media", "4", "5 media", "6"],
+ process: ["1", "1", "3", "4", "1,4,5", "5"],
+ result: "0,3,5,1,2,4",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5", "6"],
+ process: ["1", "1", "1", "2", "1,3,4,5,6,7,8", "1"],
+ result: "0,1,2,3,4,5",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5", "6", "7", "8"],
+ process: ["1", "1", "1", "2", "1,3,4,5,6,7,8", "1", "1", "1"],
+ result: "4,0,3,1,2,5,6,7",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5 selected", "6"],
+ process: ["1", "1", "1", "2", "1,3,4,5,6,7,8", "1"],
+ result: "0,1,2,3,5,4",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5 media", "6"],
+ process: ["1", "1", "1", "2", "1,3,4,5,6,7,8", "1"],
+ result: "0,1,2,3,5,4",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5 media", "6", "7", "8"],
+ process: ["1", "1", "1", "2", "1,3,4,5,6,7,8", "1", "1", "1"],
+ result: "0,3,1,2,5,6,7,4",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5 media", "6", "7", "8"],
+ process: ["1", "1,3,4,5,6,7,8", "1", "1", "1", "1", "1", "1"],
+ result: "1,0,2,3,5,6,7,4",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5 media", "6", "7", "8"],
+ process: ["1", "1", "1,3,4,5,6,7,8", "1", "1", "1", "1", "1"],
+ result: "2,0,1,3,5,6,7,4",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5 media", "6", "7", "8"],
+ process: ["1", "1", "1,1,1,1,1,1,1", "1", "1", "1", "1,1,1,1,1", "1"],
+ result: "0,1,2,3,5,6,7,4",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5 media", "6", "7", "8"],
+ process: ["1", "1", "1,2,3,4,5", "1", "1", "1", "1,2,3,4,5", "1"],
+ result: "0,1,2,3,5,6,7,4",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5 media", "6", "7", "8"],
+ process: ["1", "1", "1,6", "1", "1", "1", "1,2,3,4,5", "1"],
+ result: "0,2,1,3,5,6,7,4",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5 media", "6", "7", "8"],
+ process: ["1", "1", "1,6", "1,7", "1,8", "1,9", "1,2,3,4,5", "1"],
+ result: "2,3,0,5,1,6,7,4",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5 media", "6", "7", "8"],
+ process: ["1,10,11", "1", "1,2", "1,7", "1,8", "1,9", "1,2,3,4,5", "1"],
+ result: "0,3,1,5,2,6,7,4",
+ },
+ {
+ tabs: [
+ "1 media",
+ "2 media",
+ "3 media",
+ "4 media",
+ "5 media",
+ "6",
+ "7",
+ "8",
+ ],
+ process: ["1,10,11", "1", "1,2", "1,7", "1,8", "1,9", "1,2,3,4,5", "1"],
+ result: "6,5,7,0,1,2,3,4",
+ },
+ {
+ tabs: ["1", "2", "3"],
+ process: ["1", "2", "3"],
+ memory: ["100", "200", "300"],
+ result: "0,1,2",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
+ process: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
+ memory: [
+ "100",
+ "200",
+ "300",
+ "400",
+ "500",
+ "600",
+ "700",
+ "800",
+ "900",
+ "1000",
+ ],
+ result: "0,1,2,3,4,5,6,7,8,9",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
+ process: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
+ memory: [
+ "100",
+ "900",
+ "300",
+ "500",
+ "400",
+ "700",
+ "600",
+ "1000",
+ "200",
+ "200",
+ ],
+ result: "1,0,2,3,5,4,6,7,8,9",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
+ process: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
+ memory: [
+ "1000",
+ "900",
+ "300",
+ "500",
+ "400",
+ "1000",
+ "600",
+ "1000",
+ "200",
+ "200",
+ ],
+ result: "0,1,2,3,5,4,6,7,8,9",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5", "6"],
+ process: ["1", "2,7", "3", "4", "5", "6"],
+ memory: ["100", "200", "300", "400", "500", "600", "700"],
+ result: "1,0,2,3,4,5",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5", "6", "7", "8"],
+ process: ["1,6", "2,7", "3,8", "4,1,2", "5", "6", "7", "8"],
+ memory: ["100", "200", "300", "400", "500", "600", "700", "800"],
+ result: "2,3,0,1,4,5,6,7",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5", "6", "7", "8"],
+ process: ["1", "1", "1", "2", "1", "1", "1", "1"],
+ memory: ["700", "1000"],
+ result: "0,3,1,2,4,5,6,7",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5", "6", "7", "8"],
+ process: ["1", "1", "1", "1", "2,1", "2,1", "3", "3"],
+ memory: ["1000", "2000", "3000"],
+ result: "0,1,2,4,3,5,6,7",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5", "6", "7", "8"],
+ process: ["2", "2", "2", "2", "2,1", "2,1", "3", "3"],
+ memory: ["1000", "600", "1000"],
+ result: "0,1,2,4,3,5,6,7",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5", "6", "7", "8"],
+ process: ["1", "1", "1", "2", "2,1,1,1", "2,1", "3", "3"],
+ memory: ["1000", "1800", "1000"],
+ result: "0,1,3,2,4,5,6,7",
+ },
+ {
+ tabs: ["1", "2", "3", "4", "5", "6", "7", "8"],
+ process: ["1", "1", "1", "2", "2,1,1,1", "2,1", "3", "3"],
+ memory: ["4000", "1800", "1000"],
+ result: "0,1,2,4,3,5,6,7",
+ },
+ {
+ // The tab "1" contains 4 frames, but its uniqueCount is 1 because
+ // all of those frames are backed by the process "1". As a result,
+ // TabUnloader puts the tab "1" first based on the last access time.
+ tabs: ["1", "2", "3", "4", "5"],
+ process: ["1,1,1,1", "2", "3", "3", "3"],
+ memory: ["100", "100", "100"],
+ result: "0,1,2,3,4",
+ },
+ {
+ // The uniqueCount of the tab "1", "2", and "3" is 1, 2, and 3,
+ // respectively. As a result the first three tabs are sorted as 2,1,0.
+ tabs: ["1", "2", "3", "4", "5", "6"],
+ process: ["1,7,1,7,1,1,7,1", "7,3,7,2", "4,5,7,4,6,7", "7", "7", "7"],
+ memory: ["100", "100", "100", "100", "100", "100", "100"],
+ result: "2,1,0,3,4,5",
+ },
+];
+
+let globalBrowser = {
+ discardBrowser() {
+ return true;
+ },
+};
+
+add_task(async function doTests() {
+ for (let test of unloadTests) {
+ function* iterateTabs() {
+ let tabs = test.tabs;
+ for (let t = 0; t < tabs.length; t++) {
+ let tab = {
+ tab: {
+ originalIndex: t,
+ lastAccessed: Number(/^[0-9]+/.exec(tabs[t])[0]),
+ keywords: tabs[t],
+ process: "process" in test ? test.process[t] : "1",
+ },
+ memory: test.memory,
+ gBrowser: globalBrowser,
+ };
+ yield tab;
+ }
+ }
+ TestTabUnloaderMethods.iterateTabs = iterateTabs;
+
+ let expectedOrder = "";
+ const sortedTabs = await TabUnloader.getSortedTabs(
+ 0,
+ TestTabUnloaderMethods
+ );
+ for (let tab of sortedTabs) {
+ if (expectedOrder) {
+ expectedOrder += ",";
+ }
+ expectedOrder += tab.tab.originalIndex;
+ }
+
+ Assert.equal(expectedOrder, test.result);
+ }
+});
diff --git a/browser/modules/test/unit/test_discovery.js b/browser/modules/test/unit/test_discovery.js
new file mode 100644
index 0000000000..7237b78c20
--- /dev/null
+++ b/browser/modules/test/unit/test_discovery.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+// ClientID fails without...
+do_get_profile();
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+const { ClientID } = ChromeUtils.importESModule(
+ "resource://gre/modules/ClientID.sys.mjs"
+);
+const { Discovery } = ChromeUtils.import("resource:///modules/Discovery.jsm");
+const { ContextualIdentityService } = ChromeUtils.importESModule(
+ "resource://gre/modules/ContextualIdentityService.sys.mjs"
+);
+
+const TAAR_COOKIE_NAME = "taarId";
+
+add_task(async function test_discovery() {
+ let uri = Services.io.newURI("https://example.com/foobar");
+
+ // Ensure the prefs we need
+ Services.prefs.setBoolPref("browser.discovery.enabled", true);
+ Services.prefs.setBoolPref("browser.discovery.containers.enabled", true);
+ Services.prefs.setBoolPref("datareporting.healthreport.uploadEnabled", true);
+ Services.prefs.setCharPref("browser.discovery.sites", uri.host);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.discovery.enabled");
+ Services.prefs.clearUserPref("browser.discovery.containers.enabled");
+ Services.prefs.clearUserPref("browser.discovery.sites");
+ Services.prefs.clearUserPref("datareporting.healthreport.uploadEnabled");
+ });
+
+ // This is normally initialized by telemetry, force id creation. This results
+ // in Discovery setting the cookie.
+ await ClientID.getClientID();
+ await Discovery.update();
+
+ ok(
+ Services.cookies.cookieExists(uri.host, "/", TAAR_COOKIE_NAME, {}),
+ "cookie exists"
+ );
+ ok(
+ !Services.cookies.cookieExists(uri.host, "/", TAAR_COOKIE_NAME, {
+ privateBrowsingId: 1,
+ }),
+ "no private cookie exists"
+ );
+ ContextualIdentityService.getPublicIdentities().forEach(identity => {
+ let { userContextId } = identity;
+ equal(
+ Services.cookies.cookieExists(uri.host, "/", TAAR_COOKIE_NAME, {
+ userContextId,
+ }),
+ identity.public,
+ "cookie exists"
+ );
+ });
+
+ // Test the addition of a new container.
+ let changed = TestUtils.topicObserved("cookie-changed", (subject, data) => {
+ let cookie = subject.QueryInterface(Ci.nsICookie);
+ equal(cookie.name, TAAR_COOKIE_NAME, "taar cookie exists");
+ equal(cookie.host, uri.host, "cookie exists for host");
+ equal(
+ cookie.originAttributes.userContextId,
+ container.userContextId,
+ "cookie userContextId is correct"
+ );
+ return true;
+ });
+ let container = ContextualIdentityService.create(
+ "New Container",
+ "Icon",
+ "Color"
+ );
+ await changed;
+
+ // Test disabling
+ Discovery.enabled = false;
+ // Wait for the update to remove the cookie.
+ await TestUtils.waitForCondition(() => {
+ return !Services.cookies.cookieExists(uri.host, "/", TAAR_COOKIE_NAME, {});
+ });
+
+ ContextualIdentityService.getPublicIdentities().forEach(identity => {
+ let { userContextId } = identity;
+ ok(
+ !Services.cookies.cookieExists(uri.host, "/", TAAR_COOKIE_NAME, {
+ userContextId,
+ }),
+ "no cookie exists"
+ );
+ });
+
+ // turn off containers
+ Services.prefs.setBoolPref("browser.discovery.containers.enabled", false);
+
+ Discovery.enabled = true;
+ await TestUtils.waitForCondition(() => {
+ return Services.cookies.cookieExists(uri.host, "/", TAAR_COOKIE_NAME, {});
+ });
+ // make sure we did not set cookies on containers
+ ContextualIdentityService.getPublicIdentities().forEach(identity => {
+ let { userContextId } = identity;
+ ok(
+ !Services.cookies.cookieExists(uri.host, "/", TAAR_COOKIE_NAME, {
+ userContextId,
+ }),
+ "no cookie exists"
+ );
+ });
+
+ // Make sure clientId changes update discovery
+ changed = TestUtils.topicObserved("cookie-changed", (subject, data) => {
+ if (data !== "added") {
+ return false;
+ }
+ let cookie = subject.QueryInterface(Ci.nsICookie);
+ equal(cookie.name, TAAR_COOKIE_NAME, "taar cookie exists");
+ equal(cookie.host, uri.host, "cookie exists for host");
+ return true;
+ });
+ await ClientID.removeClientID();
+ await ClientID.getClientID();
+ await changed;
+
+ // Make sure disabling telemetry disables discovery.
+ Services.prefs.setBoolPref("datareporting.healthreport.uploadEnabled", false);
+ await TestUtils.waitForCondition(() => {
+ return !Services.cookies.cookieExists(uri.host, "/", TAAR_COOKIE_NAME, {});
+ });
+});
diff --git a/browser/modules/test/unit/xpcshell.ini b/browser/modules/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..d7bda83c77
--- /dev/null
+++ b/browser/modules/test/unit/xpcshell.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+head =
+firefox-appdir = browser
+skip-if = toolkit == 'android' # bug 1730213
+
+[test_E10SUtils_nested_URIs.js]
+[test_HomePage.js]
+[test_HomePage_ignore.js]
+[test_Sanitizer_interrupted.js]
+[test_SitePermissions.js]
+[test_SitePermissions_temporary.js]
+[test_SiteDataManager.js]
+[test_SiteDataManagerContainers.js]
+[test_TabUnloader.js]
+[test_LaterRun.js]
+[test_discovery.js]
+[test_PingCentre.js]
+[test_ProfileCounter.js]
+skip-if = os != 'win' # Test of a Windows-specific feature
+[test_InstallationTelemetry.js]
+skip-if =
+ os != 'win' # Test of a Windows-specific feature
+[test_PartnerLinkAttribution.js]