diff options
Diffstat (limited to '')
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] |