From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../extensions/test/xpcshell/.eslintrc.js | 9 + .../test/xpcshell/data/test/manifest.json | 80 + .../test/xpcshell/data/test2/manifest.json | 23 + .../components/extensions/test/xpcshell/head.js | 78 + .../extensions/test/xpcshell/test_ext_bookmarks.js | 1725 ++++++++++++++++++++ .../xpcshell/test_ext_browsingData_downloads.js | 126 ++ .../xpcshell/test_ext_browsingData_passwords.js | 96 ++ .../xpcshell/test_ext_browsingData_settings.js | 147 ++ .../test_ext_chrome_settings_overrides_home.js | 231 +++ .../test_ext_chrome_settings_overrides_update.js | 794 +++++++++ .../test/xpcshell/test_ext_distribution_popup.js | 56 + .../extensions/test/xpcshell/test_ext_history.js | 864 ++++++++++ .../test_ext_homepage_overrides_private.js | 134 ++ .../extensions/test/xpcshell/test_ext_manifest.js | 105 ++ .../test/xpcshell/test_ext_manifest_commands.js | 52 + .../test/xpcshell/test_ext_manifest_omnibox.js | 62 + .../test/xpcshell/test_ext_manifest_permissions.js | 85 + .../test/xpcshell/test_ext_menu_caller.js | 53 + .../test/xpcshell/test_ext_menu_startup.js | 432 +++++ .../test/xpcshell/test_ext_normandyAddonStudy.js | 243 +++ .../test/xpcshell/test_ext_pageAction_shutdown.js | 81 + .../test/xpcshell/test_ext_pkcs11_management.js | 300 ++++ .../test_ext_settings_overrides_defaults.js | 263 +++ .../xpcshell/test_ext_settings_overrides_search.js | 597 +++++++ .../test_ext_settings_overrides_search_mozParam.js | 239 +++ .../test_ext_settings_overrides_shutdown.js | 109 ++ .../test/xpcshell/test_ext_settings_validate.js | 193 +++ .../extensions/test/xpcshell/test_ext_topSites.js | 293 ++++ .../test/xpcshell/test_ext_url_overrides_newtab.js | 340 ++++ .../test_ext_url_overrides_newtab_update.js | 127 ++ .../extensions/test/xpcshell/xpcshell.toml | 69 + 31 files changed, 8006 insertions(+) create mode 100644 browser/components/extensions/test/xpcshell/.eslintrc.js create mode 100644 browser/components/extensions/test/xpcshell/data/test/manifest.json create mode 100644 browser/components/extensions/test/xpcshell/data/test2/manifest.json create mode 100644 browser/components/extensions/test/xpcshell/head.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_bookmarks.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_browsingData_downloads.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_browsingData_passwords.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_browsingData_settings.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_home.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_distribution_popup.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_history.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_manifest.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_menu_caller.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_menu_startup.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_normandyAddonStudy.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_settings_overrides_defaults.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search_mozParam.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_settings_overrides_shutdown.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_settings_validate.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_topSites.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab.js create mode 100644 browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab_update.js create mode 100644 browser/components/extensions/test/xpcshell/xpcshell.toml (limited to 'browser/components/extensions/test/xpcshell') diff --git a/browser/components/extensions/test/xpcshell/.eslintrc.js b/browser/components/extensions/test/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..3622fff4f6 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/.eslintrc.js @@ -0,0 +1,9 @@ +"use strict"; + +module.exports = { + env: { + // The tests in this folder are testing based on WebExtensions, so lets + // just define the webextensions environment here. + webextensions: true, + }, +}; diff --git a/browser/components/extensions/test/xpcshell/data/test/manifest.json b/browser/components/extensions/test/xpcshell/data/test/manifest.json new file mode 100644 index 0000000000..b14c90e9c4 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/data/test/manifest.json @@ -0,0 +1,80 @@ +{ + "name": "MozParamsTest", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "test@search.mozilla.org" + } + }, + "description": "A test search engine (based on Google search)", + "chrome_settings_overrides": { + "search_provider": { + "name": "MozParamsTest", + "search_url": "https://example.com/?q={searchTerms}", + "params": [ + { + "name": "test-0", + "condition": "purpose", + "purpose": "contextmenu", + "value": "0" + }, + { + "name": "test-1", + "condition": "purpose", + "purpose": "searchbar", + "value": "1" + }, + { + "name": "test-2", + "condition": "purpose", + "purpose": "homepage", + "value": "2" + }, + { + "name": "test-3", + "condition": "purpose", + "purpose": "keyword", + "value": "3" + }, + { + "name": "test-4", + "condition": "purpose", + "purpose": "newtab", + "value": "4" + }, + { + "name": "simple", + "value": "5" + }, + { + "name": "term", + "value": "{searchTerms}" + }, + { + "name": "lang", + "value": "{language}" + }, + { + "name": "locale", + "value": "{moz:locale}" + }, + { + "name": "prefval", + "condition": "pref", + "pref": "code" + }, + { + "name": "experimenter-1", + "condition": "pref", + "pref": "nimbus-key-1" + }, + { + "name": "experimenter-2", + "condition": "pref", + "pref": "nimbus-key-2" + } + ] + } + } +} diff --git a/browser/components/extensions/test/xpcshell/data/test2/manifest.json b/browser/components/extensions/test/xpcshell/data/test2/manifest.json new file mode 100644 index 0000000000..197a993189 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/data/test2/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "MozParamsTest2", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "test2@search.mozilla.org" + } + }, + "description": "A second test search engine", + "chrome_settings_overrides": { + "search_provider": { + "name": "MozParamsTest2", + "search_url": "https://example.com/2/?q={searchTerms}", + "params": [ + { + "name": "simple2", + "value": "5" + } + ] + } + } +} diff --git a/browser/components/extensions/test/xpcshell/head.js b/browser/components/extensions/test/xpcshell/head.js new file mode 100644 index 0000000000..9ac33637ed --- /dev/null +++ b/browser/components/extensions/test/xpcshell/head.js @@ -0,0 +1,78 @@ +"use strict"; + +/* exported createHttpServer, promiseConsoleOutput, assertPersistentListeners */ + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +// eslint-disable-next-line no-unused-vars +ChromeUtils.defineESModuleGetters(this, { + Extension: "resource://gre/modules/Extension.sys.mjs", + ExtensionData: "resource://gre/modules/Extension.sys.mjs", + ExtensionTestUtils: + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs", + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + HttpServer: "resource://testing-common/httpd.sys.mjs", + NetUtil: "resource://gre/modules/NetUtil.sys.mjs", + Schemas: "resource://gre/modules/Schemas.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", +}); + +ExtensionTestUtils.init(this); + +// Persistent Listener test functionality +const { assertPersistentListeners } = ExtensionTestUtils.testAssertions; + +/** + * Creates a new HttpServer for testing, and begins listening on the + * specified port. Automatically shuts down the server when the test + * unit ends. + * + * @param {integer} [port] + * The port to listen on. If omitted, listen on a random + * port. The latter is the preferred behavior. + * + * @returns {HttpServer} + */ +function createHttpServer(port = -1) { + let server = new HttpServer(); + server.start(port); + + registerCleanupFunction(() => { + return new Promise(resolve => { + server.stop(resolve); + }); + }); + + return server; +} + +var promiseConsoleOutput = async function (task) { + const DONE = `=== console listener ${Math.random()} done ===`; + + let listener; + let messages = []; + let awaitListener = new Promise(resolve => { + listener = msg => { + if (msg == DONE) { + resolve(); + } else { + void (msg instanceof Ci.nsIConsoleMessage); + messages.push(msg); + } + }; + }); + + Services.console.registerListener(listener); + try { + let result = await task(); + + Services.console.logStringMessage(DONE); + await awaitListener; + + return { messages, result }; + } finally { + Services.console.unregisterListener(listener); + } +}; diff --git a/browser/components/extensions/test/xpcshell/test_ext_bookmarks.js b/browser/components/extensions/test/xpcshell/test_ext_bookmarks.js new file mode 100644 index 0000000000..15d09d1163 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_bookmarks.js @@ -0,0 +1,1725 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", +}); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "43" +); + +add_task(async function test_bookmarks() { + async function background() { + let unsortedId, ourId; + let initialBookmarkCount = 0; + let createdBookmarks = new Set(); + let createdFolderId; + let createdSeparatorId; + let collectedEvents = []; + const nonExistentId = "000000000000"; + const bookmarkGuids = { + menuGuid: "menu________", + toolbarGuid: "toolbar_____", + unfiledGuid: "unfiled_____", + rootGuid: "root________", + }; + + function checkOurBookmark(bookmark) { + browser.test.assertEq(ourId, bookmark.id, "Bookmark has the expected Id"); + browser.test.assertTrue( + "parentId" in bookmark, + "Bookmark has a parentId" + ); + browser.test.assertEq( + 0, + bookmark.index, + "Bookmark has the expected index" + ); // We assume there are no other bookmarks. + browser.test.assertEq( + "http://example.org/", + bookmark.url, + "Bookmark has the expected url" + ); + browser.test.assertEq( + "test bookmark", + bookmark.title, + "Bookmark has the expected title" + ); + browser.test.assertTrue( + "dateAdded" in bookmark, + "Bookmark has a dateAdded" + ); + browser.test.assertFalse( + "dateGroupModified" in bookmark, + "Bookmark does not have a dateGroupModified" + ); + browser.test.assertFalse( + "unmodifiable" in bookmark, + "Bookmark is not unmodifiable" + ); + browser.test.assertEq( + "bookmark", + bookmark.type, + "Bookmark is of type bookmark" + ); + } + + function checkBookmark(expected, bookmark) { + browser.test.assertEq( + expected.url, + bookmark.url, + "Bookmark has the expected url" + ); + browser.test.assertEq( + expected.title, + bookmark.title, + "Bookmark has the expected title" + ); + browser.test.assertEq( + expected.index, + bookmark.index, + "Bookmark has expected index" + ); + browser.test.assertEq( + "bookmark", + bookmark.type, + "Bookmark is of type bookmark" + ); + if ("parentId" in expected) { + browser.test.assertEq( + expected.parentId, + bookmark.parentId, + "Bookmark has the expected parentId" + ); + } + } + + function checkOnCreated( + id, + parentId, + index, + title, + url, + dateAdded, + type = "bookmark" + ) { + let createdData = collectedEvents.pop(); + browser.test.assertEq( + "onCreated", + createdData.event, + "onCreated was the last event received" + ); + browser.test.assertEq( + id, + createdData.id, + "onCreated event received the expected id" + ); + let bookmark = createdData.bookmark; + browser.test.assertEq( + id, + bookmark.id, + "onCreated event received the expected bookmark id" + ); + browser.test.assertEq( + parentId, + bookmark.parentId, + "onCreated event received the expected bookmark parentId" + ); + browser.test.assertEq( + index, + bookmark.index, + "onCreated event received the expected bookmark index" + ); + browser.test.assertEq( + title, + bookmark.title, + "onCreated event received the expected bookmark title" + ); + browser.test.assertEq( + url, + bookmark.url, + "onCreated event received the expected bookmark url" + ); + browser.test.assertEq( + dateAdded, + bookmark.dateAdded, + "onCreated event received the expected bookmark dateAdded" + ); + browser.test.assertEq( + type, + bookmark.type, + "onCreated event received the expected bookmark type" + ); + } + + function checkOnChanged(id, url, title) { + // If both url and title are changed, then url is fired last. + let changedData = collectedEvents.pop(); + browser.test.assertEq( + "onChanged", + changedData.event, + "onChanged was the last event received" + ); + browser.test.assertEq( + id, + changedData.id, + "onChanged event received the expected id" + ); + browser.test.assertEq( + url, + changedData.info.url, + "onChanged event received the expected url" + ); + // title is fired first. + changedData = collectedEvents.pop(); + browser.test.assertEq( + "onChanged", + changedData.event, + "onChanged was the last event received" + ); + browser.test.assertEq( + id, + changedData.id, + "onChanged event received the expected id" + ); + browser.test.assertEq( + title, + changedData.info.title, + "onChanged event received the expected title" + ); + } + + function checkOnMoved(id, parentId, oldParentId, index, oldIndex) { + let movedData = collectedEvents.pop(); + browser.test.assertEq( + "onMoved", + movedData.event, + "onMoved was the last event received" + ); + browser.test.assertEq( + id, + movedData.id, + "onMoved event received the expected id" + ); + let info = movedData.info; + browser.test.assertEq( + parentId, + info.parentId, + "onMoved event received the expected parentId" + ); + browser.test.assertEq( + oldParentId, + info.oldParentId, + "onMoved event received the expected oldParentId" + ); + browser.test.assertEq( + index, + info.index, + "onMoved event received the expected index" + ); + browser.test.assertEq( + oldIndex, + info.oldIndex, + "onMoved event received the expected oldIndex" + ); + } + + function checkOnRemoved(id, parentId, index, title, url, type = "folder") { + let removedData = collectedEvents.pop(); + browser.test.assertEq( + "onRemoved", + removedData.event, + "onRemoved was the last event received" + ); + browser.test.assertEq( + id, + removedData.id, + "onRemoved event received the expected id" + ); + let info = removedData.info; + browser.test.assertEq( + parentId, + removedData.info.parentId, + "onRemoved event received the expected parentId" + ); + browser.test.assertEq( + index, + removedData.info.index, + "onRemoved event received the expected index" + ); + let node = info.node; + browser.test.assertEq( + id, + node.id, + "onRemoved event received the expected node id" + ); + browser.test.assertEq( + parentId, + node.parentId, + "onRemoved event received the expected node parentId" + ); + browser.test.assertEq( + index, + node.index, + "onRemoved event received the expected node index" + ); + browser.test.assertEq( + url, + node.url, + "onRemoved event received the expected node url" + ); + browser.test.assertEq( + title, + node.title, + "onRemoved event received the expected node title" + ); + browser.test.assertEq( + type, + node.type, + "onRemoved event received the expected node type" + ); + } + + browser.bookmarks.onChanged.addListener((id, info) => { + collectedEvents.push({ event: "onChanged", id, info }); + }); + + browser.bookmarks.onCreated.addListener((id, bookmark) => { + collectedEvents.push({ event: "onCreated", id, bookmark }); + }); + + browser.bookmarks.onMoved.addListener((id, info) => { + collectedEvents.push({ event: "onMoved", id, info }); + }); + + browser.bookmarks.onRemoved.addListener((id, info) => { + collectedEvents.push({ event: "onRemoved", id, info }); + }); + + await browser.test.assertRejects( + browser.bookmarks.get(["not-a-bookmark-guid"]), + /Invalid value for property 'guid': "not-a-bookmark-guid"/, + "Expected error thrown when trying to get a bookmark using an invalid guid" + ); + + await browser.test + .assertRejects( + browser.bookmarks.get([nonExistentId]), + /Bookmark not found/, + "Expected error thrown when trying to get a bookmark using a non-existent Id" + ) + .then(() => { + return browser.bookmarks.search({}); + }) + .then(results => { + initialBookmarkCount = results.length; + return browser.bookmarks.create({ + title: "test bookmark", + url: "http://example.org", + type: "bookmark", + }); + }) + .then(result => { + ourId = result.id; + checkOurBookmark(result); + browser.test.assertEq( + 1, + collectedEvents.length, + "1 expected event received" + ); + checkOnCreated( + ourId, + bookmarkGuids.unfiledGuid, + 0, + "test bookmark", + "http://example.org/", + result.dateAdded + ); + + return browser.bookmarks.get(ourId); + }) + .then(results => { + browser.test.assertEq(results.length, 1); + checkOurBookmark(results[0]); + + unsortedId = results[0].parentId; + return browser.bookmarks.get(unsortedId); + }) + .then(results => { + let folder = results[0]; + browser.test.assertEq(1, results.length, "1 bookmark was returned"); + + browser.test.assertEq( + unsortedId, + folder.id, + "Folder has the expected id" + ); + browser.test.assertTrue("parentId" in folder, "Folder has a parentId"); + browser.test.assertTrue("index" in folder, "Folder has an index"); + browser.test.assertEq( + undefined, + folder.url, + "Folder does not have a url" + ); + browser.test.assertEq( + "Other Bookmarks", + folder.title, + "Folder has the expected title" + ); + browser.test.assertTrue( + "dateAdded" in folder, + "Folder has a dateAdded" + ); + browser.test.assertTrue( + "dateGroupModified" in folder, + "Folder has a dateGroupModified" + ); + browser.test.assertFalse( + "unmodifiable" in folder, + "Folder is not unmodifiable" + ); // TODO: Do we want to enable this? + browser.test.assertEq( + "folder", + folder.type, + "Folder has a type of folder" + ); + + return browser.bookmarks.getChildren(unsortedId); + }) + .then(async results => { + browser.test.assertEq(1, results.length, "The folder has one child"); + checkOurBookmark(results[0]); + + await browser.test.assertRejects( + browser.bookmarks.update(nonExistentId, { title: "new test title" }), + /No bookmarks found for the provided GUID/, + "Expected error thrown when trying to update a non-existent bookmark" + ); + return browser.bookmarks.update(ourId, { + title: "new test title", + url: "http://example.com/", + }); + }) + .then(async result => { + browser.test.assertEq( + "new test title", + result.title, + "Updated bookmark has the expected title" + ); + browser.test.assertEq( + "http://example.com/", + result.url, + "Updated bookmark has the expected URL" + ); + browser.test.assertEq( + ourId, + result.id, + "Updated bookmark has the expected id" + ); + browser.test.assertEq( + "bookmark", + result.type, + "Updated bookmark has a type of bookmark" + ); + + browser.test.assertEq( + 2, + collectedEvents.length, + "2 expected events received" + ); + checkOnChanged(ourId, "http://example.com/", "new test title"); + + await browser.test.assertRejects( + browser.bookmarks.update(ourId, { url: "this is not a valid url" }), + /Invalid bookmark:/, + "Expected error thrown when trying update with an invalid url" + ); + return browser.bookmarks.getTree(); + }) + .then(results => { + browser.test.assertEq(1, results.length, "getTree returns one result"); + let bookmark = results[0].children.find( + bookmarkItem => bookmarkItem.id == unsortedId + ); + browser.test.assertEq( + "Other Bookmarks", + bookmark.title, + "Folder returned from getTree has the expected title" + ); + browser.test.assertEq( + "folder", + bookmark.type, + "Folder returned from getTree has the expected type" + ); + + return browser.test.assertRejects( + browser.bookmarks.create({ parentId: "invalid" }), + error => + error.message.includes("Invalid bookmark") && + error.message.includes(`"parentGuid":"invalid"`), + "Expected error thrown when trying to create a bookmark with an invalid parentId" + ); + }) + .then(() => { + return browser.bookmarks.remove(ourId); + }) + .then(result => { + browser.test.assertEq( + undefined, + result, + "Removing a bookmark returns undefined" + ); + + browser.test.assertEq( + 1, + collectedEvents.length, + "1 expected events received" + ); + checkOnRemoved( + ourId, + bookmarkGuids.unfiledGuid, + 0, + "new test title", + "http://example.com/", + "bookmark" + ); + + return browser.test.assertRejects( + browser.bookmarks.get(ourId), + /Bookmark not found/, + "Expected error thrown when trying to get a removed bookmark" + ); + }) + .then(() => { + return browser.test.assertRejects( + browser.bookmarks.remove(nonExistentId), + /No bookmarks found for the provided GUID/, + "Expected error thrown when trying removed a non-existent bookmark" + ); + }) + .then(() => { + // test bookmarks.search + return Promise.all([ + browser.bookmarks.create({ + title: "Μοζιλλας", + url: "http://møzîllä.örg/", + }), + browser.bookmarks.create({ + title: "Example", + url: "http://example.org/", + }), + browser.bookmarks.create({ title: "Mozilla Folder", type: "folder" }), + browser.bookmarks.create({ title: "EFF", url: "http://eff.org/" }), + browser.bookmarks.create({ + title: "Menu Item", + url: "http://menu.org/", + parentId: bookmarkGuids.menuGuid, + }), + browser.bookmarks.create({ + title: "Toolbar Item", + url: "http://toolbar.org/", + parentId: bookmarkGuids.toolbarGuid, + }), + ]); + }) + .then(results => { + browser.test.assertEq( + 6, + collectedEvents.length, + "6 expected events received" + ); + checkOnCreated( + results[5].id, + bookmarkGuids.toolbarGuid, + 0, + "Toolbar Item", + "http://toolbar.org/", + results[5].dateAdded + ); + checkOnCreated( + results[4].id, + bookmarkGuids.menuGuid, + 0, + "Menu Item", + "http://menu.org/", + results[4].dateAdded + ); + checkOnCreated( + results[3].id, + bookmarkGuids.unfiledGuid, + 0, + "EFF", + "http://eff.org/", + results[3].dateAdded + ); + checkOnCreated( + results[2].id, + bookmarkGuids.unfiledGuid, + 0, + "Mozilla Folder", + undefined, + results[2].dateAdded, + "folder" + ); + checkOnCreated( + results[1].id, + bookmarkGuids.unfiledGuid, + 0, + "Example", + "http://example.org/", + results[1].dateAdded + ); + checkOnCreated( + results[0].id, + bookmarkGuids.unfiledGuid, + 0, + "Μοζιλλας", + "http://xn--mzll-ooa1dud.xn--rg-eka/", + results[0].dateAdded + ); + + for (let result of results) { + if (result.title !== "Mozilla Folder") { + createdBookmarks.add(result.id); + } + } + let folderResult = results[2]; + createdFolderId = folderResult.id; + return Promise.all([ + browser.bookmarks.create({ + title: "Mozilla", + url: "http://allizom.org/", + parentId: createdFolderId, + }), + browser.bookmarks.create({ + parentId: createdFolderId, + type: "separator", + }), + browser.bookmarks.create({ + title: "Mozilla Corporation", + url: "http://allizom.com/", + parentId: createdFolderId, + }), + browser.bookmarks.create({ + title: "Firefox", + url: "http://allizom.org/firefox/", + parentId: createdFolderId, + }), + ]) + .then(newBookmarks => { + browser.test.assertEq( + 4, + collectedEvents.length, + "4 expected events received" + ); + checkOnCreated( + newBookmarks[3].id, + createdFolderId, + 0, + "Firefox", + "http://allizom.org/firefox/", + newBookmarks[3].dateAdded + ); + checkOnCreated( + newBookmarks[2].id, + createdFolderId, + 0, + "Mozilla Corporation", + "http://allizom.com/", + newBookmarks[2].dateAdded + ); + checkOnCreated( + newBookmarks[1].id, + createdFolderId, + 0, + "", + "data:", + newBookmarks[1].dateAdded, + "separator" + ); + checkOnCreated( + newBookmarks[0].id, + createdFolderId, + 0, + "Mozilla", + "http://allizom.org/", + newBookmarks[0].dateAdded + ); + + return browser.bookmarks.create({ + title: "About Mozilla", + url: "http://allizom.org/about/", + parentId: createdFolderId, + index: 1, + }); + }) + .then(result => { + browser.test.assertEq( + 1, + collectedEvents.length, + "1 expected events received" + ); + checkOnCreated( + result.id, + createdFolderId, + 1, + "About Mozilla", + "http://allizom.org/about/", + result.dateAdded + ); + + // returns all items on empty object + return browser.bookmarks.search({}); + }) + .then(async bookmarksSearchResults => { + browser.test.assertTrue( + bookmarksSearchResults.length >= 10, + "At least as many bookmarks as added were returned by search({})" + ); + + await browser.test.assertRejects( + browser.bookmarks.remove(createdFolderId), + /Cannot remove a non-empty folder/, + "Expected error thrown when trying to remove a non-empty folder" + ); + return browser.bookmarks.getSubTree(createdFolderId); + }); + }) + .then(results => { + browser.test.assertEq( + 1, + results.length, + "Expected number of nodes returned by getSubTree" + ); + browser.test.assertEq( + "Mozilla Folder", + results[0].title, + "Folder has the expected title" + ); + browser.test.assertEq( + bookmarkGuids.unfiledGuid, + results[0].parentId, + "Folder has the expected parentId" + ); + browser.test.assertEq( + "folder", + results[0].type, + "Folder has the expected type" + ); + let children = results[0].children; + browser.test.assertEq( + 5, + children.length, + "Expected number of bookmarks returned by getSubTree" + ); + browser.test.assertEq( + "Firefox", + children[0].title, + "Bookmark has the expected title" + ); + browser.test.assertEq( + "bookmark", + children[0].type, + "Bookmark has the expected type" + ); + browser.test.assertEq( + "About Mozilla", + children[1].title, + "Bookmark has the expected title" + ); + browser.test.assertEq( + "bookmark", + children[1].type, + "Bookmark has the expected type" + ); + browser.test.assertEq( + 1, + children[1].index, + "Bookmark has the expected index" + ); + browser.test.assertEq( + "Mozilla Corporation", + children[2].title, + "Bookmark has the expected title" + ); + browser.test.assertEq( + "", + children[3].title, + "Separator has the expected title" + ); + browser.test.assertEq( + "data:", + children[3].url, + "Separator has the expected url" + ); + browser.test.assertEq( + "separator", + children[3].type, + "Separator has the expected type" + ); + browser.test.assertEq( + "Mozilla", + children[4].title, + "Bookmark has the expected title" + ); + + // throws an error for invalid query objects + browser.test.assertThrows( + () => browser.bookmarks.search(), + /Incorrect argument types for bookmarks.search/, + "Expected error thrown when trying to search with no arguments" + ); + + browser.test.assertThrows( + () => browser.bookmarks.search(null), + /Incorrect argument types for bookmarks.search/, + "Expected error thrown when trying to search with null as an argument" + ); + + browser.test.assertThrows( + () => browser.bookmarks.search(() => {}), + /Incorrect argument types for bookmarks.search/, + "Expected error thrown when trying to search with a function as an argument" + ); + + browser.test.assertThrows( + () => browser.bookmarks.search({ banana: "banana" }), + /an unexpected "banana" property/, + "Expected error thrown when trying to search with a banana as an argument" + ); + + browser.test.assertThrows( + () => browser.bookmarks.search({ url: "spider-man vs. batman" }), + /must match the format "url"/, + "Expected error thrown when trying to search with a illegally formatted URL" + ); + // queries the full url + return browser.bookmarks.search("http://example.org/"); + }) + .then(results => { + browser.test.assertEq( + 1, + results.length, + "Expected number of results returned for url search" + ); + checkBookmark( + { title: "Example", url: "http://example.org/", index: 2 }, + results[0] + ); + + // queries a partial url + return browser.bookmarks.search("example.org"); + }) + .then(results => { + browser.test.assertEq( + 1, + results.length, + "Expected number of results returned for url search" + ); + checkBookmark( + { title: "Example", url: "http://example.org/", index: 2 }, + results[0] + ); + + // queries the title + return browser.bookmarks.search("EFF"); + }) + .then(results => { + browser.test.assertEq( + 1, + results.length, + "Expected number of results returned for title search" + ); + checkBookmark( + { + title: "EFF", + url: "http://eff.org/", + index: 0, + parentId: bookmarkGuids.unfiledGuid, + }, + results[0] + ); + + // finds menu items + return browser.bookmarks.search("Menu Item"); + }) + .then(results => { + browser.test.assertEq( + 1, + results.length, + "Expected number of results returned for menu item search" + ); + checkBookmark( + { + title: "Menu Item", + url: "http://menu.org/", + index: 0, + parentId: bookmarkGuids.menuGuid, + }, + results[0] + ); + + // finds toolbar items + return browser.bookmarks.search("Toolbar Item"); + }) + .then(results => { + browser.test.assertEq( + 1, + results.length, + "Expected number of results returned for toolbar item search" + ); + checkBookmark( + { + title: "Toolbar Item", + url: "http://toolbar.org/", + index: 0, + parentId: bookmarkGuids.toolbarGuid, + }, + results[0] + ); + + // finds folders + return browser.bookmarks.search("Mozilla Folder"); + }) + .then(results => { + browser.test.assertEq( + 1, + results.length, + "Expected number of folders returned" + ); + browser.test.assertEq( + "Mozilla Folder", + results[0].title, + "Folder has the expected title" + ); + browser.test.assertEq( + "folder", + results[0].type, + "Folder has the expected type" + ); + + // is case-insensitive + return browser.bookmarks.search("corporation"); + }) + .then(results => { + browser.test.assertEq( + 1, + results.length, + "Expected number of results returnedfor case-insensitive search" + ); + browser.test.assertEq( + "Mozilla Corporation", + results[0].title, + "Bookmark has the expected title" + ); + + // is case-insensitive for non-ascii + return browser.bookmarks.search("ΜοΖΙΛΛΑς"); + }) + .then(results => { + browser.test.assertEq( + 1, + results.length, + "Expected number of results returned for non-ascii search" + ); + browser.test.assertEq( + "Μοζιλλας", + results[0].title, + "Bookmark has the expected title" + ); + + // returns multiple results + return browser.bookmarks.search("allizom"); + }) + .then(results => { + browser.test.assertEq( + 4, + results.length, + "Expected number of multiple results returned" + ); + browser.test.assertEq( + "Mozilla", + results[0].title, + "Bookmark has the expected title" + ); + browser.test.assertEq( + "Mozilla Corporation", + results[1].title, + "Bookmark has the expected title" + ); + browser.test.assertEq( + "Firefox", + results[2].title, + "Bookmark has the expected title" + ); + browser.test.assertEq( + "About Mozilla", + results[3].title, + "Bookmark has the expected title" + ); + + // accepts a url field + return browser.bookmarks.search({ url: "http://allizom.com/" }); + }) + .then(results => { + browser.test.assertEq( + 1, + results.length, + "Expected number of results returned for url field" + ); + checkBookmark( + { + title: "Mozilla Corporation", + url: "http://allizom.com/", + index: 2, + }, + results[0] + ); + + // normalizes urls + return browser.bookmarks.search({ url: "http://allizom.com" }); + }) + .then(results => { + browser.test.assertEq( + results.length, + 1, + "Expected number of results returned for normalized url field" + ); + checkBookmark( + { + title: "Mozilla Corporation", + url: "http://allizom.com/", + index: 2, + }, + results[0] + ); + + // normalizes urls even more + return browser.bookmarks.search({ url: "http:allizom.com" }); + }) + .then(results => { + browser.test.assertEq( + results.length, + 1, + "Expected number of results returned for normalized url field" + ); + checkBookmark( + { + title: "Mozilla Corporation", + url: "http://allizom.com/", + index: 2, + }, + results[0] + ); + + // accepts a title field + return browser.bookmarks.search({ title: "Mozilla" }); + }) + .then(results => { + browser.test.assertEq( + results.length, + 1, + "Expected number of results returned for title field" + ); + checkBookmark( + { title: "Mozilla", url: "http://allizom.org/", index: 4 }, + results[0] + ); + + // can combine title and query + return browser.bookmarks.search({ title: "Mozilla", query: "allizom" }); + }) + .then(results => { + browser.test.assertEq( + 1, + results.length, + "Expected number of results returned for title and query fields" + ); + checkBookmark( + { title: "Mozilla", url: "http://allizom.org/", index: 4 }, + results[0] + ); + + // uses AND conditions + return browser.bookmarks.search({ title: "EFF", query: "allizom" }); + }) + .then(results => { + browser.test.assertEq( + 0, + results.length, + "Expected number of results returned for non-matching title and query fields" + ); + + // returns an empty array on item not found + return browser.bookmarks.search("microsoft"); + }) + .then(results => { + browser.test.assertEq( + 0, + results.length, + "Expected number of results returned for non-matching search" + ); + + browser.test.assertThrows( + () => browser.bookmarks.getRecent(""), + /Incorrect argument types for bookmarks.getRecent/, + "Expected error thrown when calling getRecent with an empty string" + ); + }) + .then(() => { + browser.test.assertThrows( + () => browser.bookmarks.getRecent(1.234), + /Incorrect argument types for bookmarks.getRecent/, + "Expected error thrown when calling getRecent with a decimal number" + ); + }) + .then(() => { + return Promise.all([ + browser.bookmarks.search("corporation"), + browser.bookmarks.getChildren(bookmarkGuids.menuGuid), + ]); + }) + .then(results => { + let corporationBookmark = results[0][0]; + let childCount = results[1].length; + + browser.test.assertEq( + 2, + corporationBookmark.index, + "Bookmark has the expected index" + ); + + return browser.bookmarks + .move(corporationBookmark.id, { index: 0 }) + .then(result => { + browser.test.assertEq( + 0, + result.index, + "Bookmark has the expected index" + ); + + browser.test.assertEq( + 1, + collectedEvents.length, + "1 expected events received" + ); + checkOnMoved( + corporationBookmark.id, + createdFolderId, + createdFolderId, + 0, + 2 + ); + + return browser.bookmarks.move(corporationBookmark.id, { + parentId: bookmarkGuids.menuGuid, + }); + }) + .then(result => { + browser.test.assertEq( + bookmarkGuids.menuGuid, + result.parentId, + "Bookmark has the expected parent" + ); + browser.test.assertEq( + childCount, + result.index, + "Bookmark has the expected index" + ); + + browser.test.assertEq( + 1, + collectedEvents.length, + "1 expected events received" + ); + checkOnMoved( + corporationBookmark.id, + bookmarkGuids.menuGuid, + createdFolderId, + 1, + 0 + ); + + return browser.bookmarks.move(corporationBookmark.id, { index: 0 }); + }) + .then(result => { + browser.test.assertEq( + bookmarkGuids.menuGuid, + result.parentId, + "Bookmark has the expected parent" + ); + browser.test.assertEq( + 0, + result.index, + "Bookmark has the expected index" + ); + + browser.test.assertEq( + 1, + collectedEvents.length, + "1 expected events received" + ); + checkOnMoved( + corporationBookmark.id, + bookmarkGuids.menuGuid, + bookmarkGuids.menuGuid, + 0, + 1 + ); + + return browser.bookmarks.move(corporationBookmark.id, { + parentId: bookmarkGuids.toolbarGuid, + index: 1, + }); + }) + .then(result => { + browser.test.assertEq( + bookmarkGuids.toolbarGuid, + result.parentId, + "Bookmark has the expected parent" + ); + browser.test.assertEq( + 1, + result.index, + "Bookmark has the expected index" + ); + + browser.test.assertEq( + 1, + collectedEvents.length, + "1 expected events received" + ); + checkOnMoved( + corporationBookmark.id, + bookmarkGuids.toolbarGuid, + bookmarkGuids.menuGuid, + 1, + 0 + ); + + createdBookmarks.add(corporationBookmark.id); + }); + }) + .then(() => { + return browser.bookmarks.getRecent(4); + }) + .then(results => { + browser.test.assertEq( + 4, + results.length, + "Expected number of results returned by getRecent" + ); + let prevDate = results[0].dateAdded; + for (let bookmark of results) { + browser.test.assertTrue( + bookmark.dateAdded <= prevDate, + "The recent bookmarks are sorted by dateAdded" + ); + prevDate = bookmark.dateAdded; + } + let bookmarksByTitle = results.sort((a, b) => { + return a.title.localeCompare(b.title); + }); + browser.test.assertEq( + "About Mozilla", + bookmarksByTitle[0].title, + "Bookmark has the expected title" + ); + browser.test.assertEq( + "Firefox", + bookmarksByTitle[1].title, + "Bookmark has the expected title" + ); + browser.test.assertEq( + "Mozilla", + bookmarksByTitle[2].title, + "Bookmark has the expected title" + ); + browser.test.assertEq( + "Mozilla Corporation", + bookmarksByTitle[3].title, + "Bookmark has the expected title" + ); + + return browser.bookmarks.search({}); + }) + .then(results => { + let startBookmarkCount = results.length; + + return browser.bookmarks + .search({ title: "Mozilla Folder" }) + .then(result => { + return browser.bookmarks.removeTree(result[0].id); + }) + .then(() => { + browser.test.assertEq( + 1, + collectedEvents.length, + "1 expected events received" + ); + checkOnRemoved( + createdFolderId, + bookmarkGuids.unfiledGuid, + 1, + "Mozilla Folder" + ); + + return browser.bookmarks.search({}).then(searchResults => { + browser.test.assertEq( + startBookmarkCount - 5, + searchResults.length, + "Expected number of results returned after removeTree" + ); + }); + }); + }) + .then(() => { + return browser.bookmarks.create({ title: "Empty Folder" }); + }) + .then(result => { + createdFolderId = result.id; + + browser.test.assertEq( + 1, + collectedEvents.length, + "1 expected events received" + ); + checkOnCreated( + createdFolderId, + bookmarkGuids.unfiledGuid, + 3, + "Empty Folder", + undefined, + result.dateAdded, + "folder" + ); + + browser.test.assertEq( + "Empty Folder", + result.title, + "Folder has the expected title" + ); + browser.test.assertEq( + "folder", + result.type, + "Folder has the expected type" + ); + + return browser.bookmarks.create({ + parentId: createdFolderId, + type: "separator", + }); + }) + .then(result => { + createdSeparatorId = result.id; + browser.test.assertEq( + 1, + collectedEvents.length, + "1 expected events received" + ); + checkOnCreated( + createdSeparatorId, + createdFolderId, + 0, + "", + "data:", + result.dateAdded, + "separator" + ); + return browser.bookmarks.remove(createdSeparatorId); + }) + .then(() => { + browser.test.assertEq( + 1, + collectedEvents.length, + "1 expected events received" + ); + checkOnRemoved( + createdSeparatorId, + createdFolderId, + 0, + "", + "data:", + "separator" + ); + + return browser.bookmarks.remove(createdFolderId); + }) + .then(() => { + browser.test.assertEq( + 1, + collectedEvents.length, + "1 expected events received" + ); + checkOnRemoved( + createdFolderId, + bookmarkGuids.unfiledGuid, + 3, + "Empty Folder" + ); + + return browser.test.assertRejects( + browser.bookmarks.get(createdFolderId), + /Bookmark not found/, + "Expected error thrown when trying to get a removed folder" + ); + }) + .then(() => { + return browser.test.assertRejects( + browser.bookmarks.getChildren(nonExistentId), + /root is null/, + "Expected error thrown when trying to getChildren for a non-existent folder" + ); + }) + .then(() => { + return browser.test.assertRejects( + browser.bookmarks.move(nonExistentId, {}), + /No bookmarks found for the provided GUID/, + "Expected error thrown when calling move with a non-existent bookmark" + ); + }) + .then(() => { + return browser.test.assertRejects( + browser.bookmarks.create({ + title: "test root folder", + parentId: bookmarkGuids.rootGuid, + }), + "The bookmark root cannot be modified", + "Expected error thrown when creating bookmark folder at the root" + ); + }) + .then(() => { + return browser.test.assertRejects( + browser.bookmarks.update(bookmarkGuids.rootGuid, { + title: "test update title", + }), + "The bookmark root cannot be modified", + "Expected error thrown when updating root" + ); + }) + .then(() => { + return browser.test.assertRejects( + browser.bookmarks.remove(bookmarkGuids.rootGuid), + "The bookmark root cannot be modified", + "Expected error thrown when removing root" + ); + }) + .then(() => { + return browser.test.assertRejects( + browser.bookmarks.removeTree(bookmarkGuids.rootGuid), + "The bookmark root cannot be modified", + "Expected error thrown when removing root tree" + ); + }) + .then(() => { + return browser.bookmarks.create({ title: "Empty Folder" }); + }) + .then(async result => { + createdFolderId = result.id; + + browser.test.assertEq( + 1, + collectedEvents.length, + "1 expected events received" + ); + checkOnCreated( + createdFolderId, + bookmarkGuids.unfiledGuid, + 3, + "Empty Folder", + undefined, + result.dateAdded, + "folder" + ); + + await browser.test.assertRejects( + browser.bookmarks.move(createdFolderId, { + parentId: bookmarkGuids.rootGuid, + }), + "The bookmark root cannot be modified", + "Expected error thrown when moving bookmark folder to the root" + ); + + return browser.bookmarks.remove(createdFolderId); + }) + .then(() => { + browser.test.assertEq( + 1, + collectedEvents.length, + "1 expected events received" + ); + checkOnRemoved( + createdFolderId, + bookmarkGuids.unfiledGuid, + 3, + "Empty Folder", + undefined, + "folder" + ); + + return browser.test.assertRejects( + browser.bookmarks.get(createdFolderId), + "Bookmark not found", + "Expected error thrown when trying to get a removed folder" + ); + }) + .then(() => { + return browser.test.assertRejects( + browser.bookmarks.move(bookmarkGuids.rootGuid, { + parentId: bookmarkGuids.unfiledGuid, + }), + "The bookmark root cannot be modified", + "Expected error thrown when moving root" + ); + }) + .then(() => { + // remove all created bookmarks + let promises = Array.from(createdBookmarks, guid => + browser.bookmarks.remove(guid) + ); + return Promise.all(promises); + }) + .then(() => { + browser.test.assertEq( + createdBookmarks.size, + collectedEvents.length, + "expected number of events received" + ); + + return browser.bookmarks.search({}); + }) + .then(results => { + browser.test.assertEq( + initialBookmarkCount, + results.length, + "All created bookmarks have been removed" + ); + + return browser.test.notifyPass("bookmarks"); + }) + .catch(error => { + browser.test.fail(`Error: ${String(error)} :: ${error.stack}`); + browser.test.notifyFail("bookmarks"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["bookmarks"], + }, + }); + + await extension.startup(); + await extension.awaitFinish("bookmarks"); + await extension.unload(); +}); + +add_task(async function test_get_recent_with_tag_and_query() { + function background() { + browser.bookmarks.getRecent(100).then(bookmarks => { + browser.test.sendMessage("bookmarks", bookmarks); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["bookmarks"], + }, + }); + + // Start with an empty bookmarks database. + await PlacesUtils.bookmarks.eraseEverything(); + + let createdBookmarks = []; + for (let i = 0; i < 3; i++) { + let bookmark = { + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: `http://example.com/${i}`, + title: `My bookmark ${i}`, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }; + createdBookmarks.unshift(bookmark); + await PlacesUtils.bookmarks.insert(bookmark); + } + + // Add a tag to the most recent url to prove it doesn't get returned. + PlacesUtils.tagging.tagURI(NetUtil.newURI("http://example.com/${i}"), [ + "Test Tag", + ]); + + // Add a query bookmark. + let queryURL = `place:parent=${PlacesUtils.bookmarks.menuGuid}&queryType=1`; + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: queryURL, + title: "a test query", + }); + + await extension.startup(); + let receivedBookmarks = await extension.awaitMessage("bookmarks"); + + equal( + receivedBookmarks.length, + 3, + "The expected number of bookmarks was returned." + ); + for (let i = 0; i < 3; i++) { + let actual = receivedBookmarks[i]; + let expected = createdBookmarks[i]; + equal(actual.url, expected.url, "Bookmark has the expected url."); + equal(actual.title, expected.title, "Bookmark has the expected title."); + equal( + actual.parentId, + expected.parentGuid, + "Bookmark has the expected parentId." + ); + } + + await extension.unload(); +}); + +add_task(async function test_tree_with_empty_folder() { + async function background() { + await browser.bookmarks.create({ title: "Empty Folder" }); + let nonEmptyFolder = await browser.bookmarks.create({ + title: "Non-Empty Folder", + }); + await browser.bookmarks.create({ + title: "A bookmark", + url: "http://example.com", + parentId: nonEmptyFolder.id, + }); + + let tree = await browser.bookmarks.getSubTree(nonEmptyFolder.parentId); + browser.test.assertEq( + 0, + tree[0].children[0].children.length, + "The empty folder returns an empty array for children." + ); + browser.test.assertEq( + 1, + tree[0].children[1].children.length, + "The non-empty folder returns a single item array for children." + ); + + let children = await browser.bookmarks.getChildren(nonEmptyFolder.parentId); + // getChildren should only return immediate children. This is not tested in the + // monster test above. + for (let child of children) { + browser.test.assertEq( + undefined, + child.children, + "Child from getChildren does not contain any children." + ); + } + + browser.test.sendMessage("done"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["bookmarks"], + }, + }); + + // Start with an empty bookmarks database. + await PlacesUtils.bookmarks.eraseEverything(); + + await extension.startup(); + await extension.awaitMessage("done"); + + await extension.unload(); +}); + +add_task( + { + pref_set: [["extensions.eventPages.enabled", true]], + }, + async function test_bookmarks_event_page() { + await AddonTestUtils.promiseStartupManager(); + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + browser_specific_settings: { gecko: { id: "eventpage@bookmarks" } }, + permissions: ["bookmarks"], + background: { persistent: false }, + }, + background() { + browser.bookmarks.onCreated.addListener(() => { + browser.test.sendMessage("onCreated"); + }); + browser.bookmarks.onRemoved.addListener(() => { + browser.test.sendMessage("onRemoved"); + }); + browser.bookmarks.onChanged.addListener(() => {}); + browser.bookmarks.onMoved.addListener(() => {}); + browser.test.sendMessage("ready"); + }, + }); + + const EVENTS = ["onCreated", "onRemoved", "onChanged", "onMoved"]; + await PlacesUtils.bookmarks.eraseEverything(); + + await extension.startup(); + await extension.awaitMessage("ready"); + for (let event of EVENTS) { + assertPersistentListeners(extension, "bookmarks", event, { + primed: false, + }); + } + + // test events waken background + await extension.terminateBackground(); + for (let event of EVENTS) { + assertPersistentListeners(extension, "bookmarks", event, { + primed: true, + }); + } + + let bookmark = { + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: `http://example.com/12345`, + title: `My bookmark 12345`, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }; + await PlacesUtils.bookmarks.insert(bookmark); + + await extension.awaitMessage("ready"); + await extension.awaitMessage("onCreated"); + for (let event of EVENTS) { + assertPersistentListeners(extension, "bookmarks", event, { + primed: false, + }); + } + + await AddonTestUtils.promiseRestartManager(); + await extension.awaitStartup(); + + for (let event of EVENTS) { + assertPersistentListeners(extension, "bookmarks", event, { + primed: true, + }); + } + + await PlacesUtils.bookmarks.eraseEverything(); + await extension.awaitMessage("ready"); + await extension.awaitMessage("onRemoved"); + + await extension.unload(); + await AddonTestUtils.promiseShutdownManager(); + } +); diff --git a/browser/components/extensions/test/xpcshell/test_ext_browsingData_downloads.js b/browser/components/extensions/test/xpcshell/test_ext_browsingData_downloads.js new file mode 100644 index 0000000000..1257f23600 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_downloads.js @@ -0,0 +1,126 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + Downloads: "resource://gre/modules/Downloads.sys.mjs", +}); + +const OLD_NAMES = { + [Downloads.PUBLIC]: "old-public", + [Downloads.PRIVATE]: "old-private", +}; +const RECENT_NAMES = { + [Downloads.PUBLIC]: "recent-public", + [Downloads.PRIVATE]: "recent-private", +}; +const REFERENCE_DATE = new Date(); +const OLD_DATE = new Date(Number(REFERENCE_DATE) - 10000); + +async function downloadExists(list, path) { + let listArray = await list.getAll(); + return listArray.some(i => i.target.path == path); +} + +async function checkDownloads( + expectOldExists = true, + expectRecentExists = true +) { + for (let listType of [Downloads.PUBLIC, Downloads.PRIVATE]) { + let downloadsList = await Downloads.getList(listType); + equal( + await downloadExists(downloadsList, OLD_NAMES[listType]), + expectOldExists, + `Fake old download ${expectOldExists ? "was found" : "was removed"}.` + ); + equal( + await downloadExists(downloadsList, RECENT_NAMES[listType]), + expectRecentExists, + `Fake recent download ${ + expectRecentExists ? "was found" : "was removed" + }.` + ); + } +} + +async function setupDownloads() { + let downloadsList = await Downloads.getList(Downloads.ALL); + await downloadsList.removeFinished(); + + for (let listType of [Downloads.PUBLIC, Downloads.PRIVATE]) { + downloadsList = await Downloads.getList(listType); + let download = await Downloads.createDownload({ + source: { + url: "https://bugzilla.mozilla.org/show_bug.cgi?id=1321303", + isPrivate: listType == Downloads.PRIVATE, + }, + target: OLD_NAMES[listType], + }); + download.startTime = OLD_DATE; + download.canceled = true; + await downloadsList.add(download); + + download = await Downloads.createDownload({ + source: { + url: "https://bugzilla.mozilla.org/show_bug.cgi?id=1321303", + isPrivate: listType == Downloads.PRIVATE, + }, + target: RECENT_NAMES[listType], + }); + download.startTime = REFERENCE_DATE; + download.canceled = true; + await downloadsList.add(download); + } + + // Confirm everything worked. + downloadsList = await Downloads.getList(Downloads.ALL); + equal((await downloadsList.getAll()).length, 4, "4 fake downloads added."); + checkDownloads(); +} + +add_task(async function testDownloads() { + function background() { + browser.test.onMessage.addListener(async (msg, options) => { + if (msg == "removeDownloads") { + await browser.browsingData.removeDownloads(options); + } else { + await browser.browsingData.remove(options, { downloads: true }); + } + browser.test.sendMessage("downloadsRemoved"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["browsingData"], + }, + }); + + async function testRemovalMethod(method) { + // Clear downloads with no since value. + await setupDownloads(); + extension.sendMessage(method, {}); + await extension.awaitMessage("downloadsRemoved"); + await checkDownloads(false, false); + + // Clear downloads with recent since value. + await setupDownloads(); + extension.sendMessage(method, { since: REFERENCE_DATE }); + await extension.awaitMessage("downloadsRemoved"); + await checkDownloads(true, false); + + // Clear downloads with old since value. + await setupDownloads(); + extension.sendMessage(method, { since: REFERENCE_DATE - 100000 }); + await extension.awaitMessage("downloadsRemoved"); + await checkDownloads(false, false); + } + + await extension.startup(); + + await testRemovalMethod("removeDownloads"); + await testRemovalMethod("remove"); + + await extension.unload(); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_browsingData_passwords.js b/browser/components/extensions/test/xpcshell/test_ext_browsingData_passwords.js new file mode 100644 index 0000000000..68a2c2cdc5 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_passwords.js @@ -0,0 +1,96 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const REFERENCE_DATE = Date.now(); +const LOGIN_USERNAME = "username"; +const LOGIN_PASSWORD = "password"; +const OLD_HOST = "http://mozilla.org"; +const NEW_HOST = "http://mozilla.com"; +const FXA_HOST = "chrome://FirefoxAccounts"; + +async function checkLoginExists(origin, shouldExist) { + const logins = await Services.logins.searchLoginsAsync({ origin }); + equal( + logins.length, + shouldExist ? 1 : 0, + `Login for origin ${origin} should ${shouldExist ? "" : "not"} be found.` + ); +} + +async function addLogin(host, timestamp) { + await checkLoginExists(host, false); + let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance( + Ci.nsILoginInfo + ); + login.init(host, "", null, LOGIN_USERNAME, LOGIN_PASSWORD); + login.QueryInterface(Ci.nsILoginMetaInfo); + login.timePasswordChanged = timestamp; + await Services.logins.addLoginAsync(login); + await checkLoginExists(host, true); +} + +async function setupPasswords() { + // Remove all logins if any (included FxAccounts one in case one got captured in + // a conditioned profile, see Bug 1853617). + Services.logins.removeAllLogins(); + await addLogin(FXA_HOST, REFERENCE_DATE); + await addLogin(NEW_HOST, REFERENCE_DATE); + await addLogin(OLD_HOST, REFERENCE_DATE - 10000); +} + +add_task(async function testPasswords() { + function background() { + browser.test.onMessage.addListener(async (msg, options) => { + if (msg == "removeHistory") { + await browser.browsingData.removePasswords(options); + } else { + await browser.browsingData.remove(options, { passwords: true }); + } + browser.test.sendMessage("passwordsRemoved"); + }); + } + + const extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["browsingData"], + }, + }); + + async function testRemovalMethod(method) { + // Clear passwords with no since value. + await setupPasswords(); + extension.sendMessage(method, {}); + await extension.awaitMessage("passwordsRemoved"); + + await checkLoginExists(OLD_HOST, false); + await checkLoginExists(NEW_HOST, false); + await checkLoginExists(FXA_HOST, true); + + // Clear passwords with recent since value. + await setupPasswords(); + extension.sendMessage(method, { since: REFERENCE_DATE - 1000 }); + await extension.awaitMessage("passwordsRemoved"); + + await checkLoginExists(OLD_HOST, true); + await checkLoginExists(NEW_HOST, false); + await checkLoginExists(FXA_HOST, true); + + // Clear passwords with old since value. + await setupPasswords(); + extension.sendMessage(method, { since: REFERENCE_DATE - 20000 }); + await extension.awaitMessage("passwordsRemoved"); + + await checkLoginExists(OLD_HOST, false); + await checkLoginExists(NEW_HOST, false); + await checkLoginExists(FXA_HOST, true); + } + + await extension.startup(); + + await testRemovalMethod("removePasswords"); + await testRemovalMethod("remove"); + + await extension.unload(); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_browsingData_settings.js b/browser/components/extensions/test/xpcshell/test_ext_browsingData_settings.js new file mode 100644 index 0000000000..9d2241895c --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_settings.js @@ -0,0 +1,147 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + Preferences: "resource://gre/modules/Preferences.sys.mjs", + Sanitizer: "resource:///modules/Sanitizer.sys.mjs", +}); + +const PREF_DOMAIN = "privacy.cpd."; +const SETTINGS_LIST = [ + "cache", + "cookies", + "history", + "formData", + "downloads", +].sort(); + +add_task(async function testSettingsProperties() { + function background() { + browser.test.onMessage.addListener(msg => { + browser.browsingData.settings().then(settings => { + browser.test.sendMessage("settings", settings); + }); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["browsingData"], + }, + }); + + await extension.startup(); + + extension.sendMessage("settings"); + let settings = await extension.awaitMessage("settings"); + + // Verify that we get the keys back we expect. + deepEqual( + Object.keys(settings.dataToRemove).sort(), + SETTINGS_LIST, + "dataToRemove contains expected properties." + ); + deepEqual( + Object.keys(settings.dataRemovalPermitted).sort(), + SETTINGS_LIST, + "dataToRemove contains expected properties." + ); + + let dataTypeSet = settings.dataToRemove; + for (let key of Object.keys(dataTypeSet)) { + equal( + Preferences.get(`${PREF_DOMAIN}${key.toLowerCase()}`), + dataTypeSet[key], + `${key} property of dataToRemove matches the expected pref.` + ); + } + + dataTypeSet = settings.dataRemovalPermitted; + for (let key of Object.keys(dataTypeSet)) { + equal( + true, + dataTypeSet[key], + `${key} property of dataRemovalPermitted is true.` + ); + } + + // Explicitly set a pref to both true and false and then check. + const SINGLE_OPTION = "cache"; + const SINGLE_PREF = "privacy.cpd.cache"; + + registerCleanupFunction(() => { + Preferences.reset(SINGLE_PREF); + }); + + Preferences.set(SINGLE_PREF, true); + + extension.sendMessage("settings"); + settings = await extension.awaitMessage("settings"); + equal( + settings.dataToRemove[SINGLE_OPTION], + true, + "Preference that was set to true returns true." + ); + + Preferences.set(SINGLE_PREF, false); + + extension.sendMessage("settings"); + settings = await extension.awaitMessage("settings"); + equal( + settings.dataToRemove[SINGLE_OPTION], + false, + "Preference that was set to false returns false." + ); + + await extension.unload(); +}); + +add_task(async function testSettingsSince() { + const TIMESPAN_PREF = "privacy.sanitize.timeSpan"; + const TEST_DATA = { + TIMESPAN_5MIN: Date.now() - 5 * 60 * 1000, + TIMESPAN_HOUR: Date.now() - 60 * 60 * 1000, + TIMESPAN_2HOURS: Date.now() - 2 * 60 * 60 * 1000, + TIMESPAN_EVERYTHING: 0, + }; + + function background() { + browser.test.onMessage.addListener(msg => { + browser.browsingData.settings().then(settings => { + browser.test.sendMessage("settings", settings); + }); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["browsingData"], + }, + }); + + await extension.startup(); + + registerCleanupFunction(() => { + Preferences.reset(TIMESPAN_PREF); + }); + + for (let timespan in TEST_DATA) { + Preferences.set(TIMESPAN_PREF, Sanitizer[timespan]); + + extension.sendMessage("settings"); + let settings = await extension.awaitMessage("settings"); + + // Because it is based on the current timestamp, we cannot know the exact + // value to expect for since, so allow a 10s variance. + Assert.less( + Math.abs(settings.options.since - TEST_DATA[timespan]), + 10000, + "settings.options contains the expected since value." + ); + } + + await extension.unload(); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_home.js b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_home.js new file mode 100644 index 0000000000..d61e5b6b5e --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_home.js @@ -0,0 +1,231 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + HomePage: "resource:///modules/HomePage.sys.mjs", + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +function promisePrefChanged(expectedValue) { + return TestUtils.waitForPrefChange("browser.startup.homepage", value => + value.endsWith(expectedValue) + ); +} + +const HOMEPAGE_EXTENSION_CONTROLLED = + "browser.startup.homepage_override.extensionControlled"; + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); + +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "42" +); + +async function setupRemoteSettings() { + const settings = await RemoteSettings("hijack-blocklists"); + sinon.stub(settings, "get").returns([ + { + id: "homepage-urls", + matches: ["ignore=me"], + _status: "synced", + }, + ]); +} + +add_task(async function setup() { + await AddonTestUtils.promiseStartupManager(); + await setupRemoteSettings(); +}); + +add_task(async function test_overriding_with_ignored_url() { + // Manually poke into the ignore list a value to be ignored. + HomePage._ignoreList.push("ignore=me"); + Services.prefs.setBoolPref(HOMEPAGE_EXTENSION_CONTROLLED, false); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { + id: "ignore_homepage@example.com", + }, + }, + chrome_settings_overrides: { homepage: "https://example.com/?ignore=me" }, + name: "extension", + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + + Assert.ok(HomePage.isDefault, "Should still have the default homepage"); + Assert.equal( + Services.prefs.getBoolPref( + "browser.startup.homepage_override.extensionControlled" + ), + false, + "Should not be extension controlled." + ); + TelemetryTestUtils.assertEvents( + [ + { + object: "ignore", + value: "set_blocked_extension", + extra: { webExtensionId: "ignore_homepage@example.com" }, + }, + ], + { + category: "homepage", + method: "preference", + } + ); + + await extension.unload(); + HomePage._ignoreList.pop(); +}); + +add_task(async function test_overriding_cancelled_after_ignore_update() { + const oldHomePageIgnoreList = HomePage._ignoreList; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { + id: "ignore_homepage1@example.com", + }, + }, + chrome_settings_overrides: { + homepage: "https://example.com/?ignore1=me", + }, + name: "extension", + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + + Assert.ok(!HomePage.isDefault, "Should have overriden the new homepage"); + Assert.equal( + Services.prefs.getBoolPref( + "browser.startup.homepage_override.extensionControlled" + ), + true, + "Should be extension controlled." + ); + + let prefChanged = TestUtils.waitForPrefChange( + "browser.startup.homepage_override.extensionControlled" + ); + + await HomePage._handleIgnoreListUpdated({ + data: { + current: [{ id: "homepage-urls", matches: ["ignore1=me"] }], + }, + }); + + await prefChanged; + + await TestUtils.waitForCondition( + () => + !Services.prefs.getBoolPref( + "browser.startup.homepage_override.extensionControlled", + false + ), + "Should not longer be extension controlled" + ); + + Assert.ok(HomePage.isDefault, "Should have reset the homepage"); + + TelemetryTestUtils.assertEvents( + [ + { + object: "ignore", + value: "saved_reset", + }, + ], + { + category: "homepage", + method: "preference", + } + ); + + await extension.unload(); + HomePage._ignoreList = oldHomePageIgnoreList; +}); + +add_task(async function test_overriding_homepage_locale() { + Services.locale.availableLocales = ["en-US", "es-ES"]; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { + id: "homepage@example.com", + }, + }, + chrome_settings_overrides: { + homepage: "/__MSG_homepage__", + }, + name: "extension", + default_locale: "en", + }, + useAddonManager: "permanent", + + files: { + "_locales/en/messages.json": { + homepage: { + message: "homepage.html", + description: "homepage", + }, + }, + + "_locales/es_ES/messages.json": { + homepage: { + message: "default.html", + description: "homepage", + }, + }, + }, + }); + + let prefPromise = promisePrefChanged("homepage.html"); + await extension.startup(); + await prefPromise; + + Assert.equal( + HomePage.get(), + `moz-extension://${extension.uuid}/homepage.html`, + "Should have overridden the new homepage" + ); + + // Set the new locale now, and disable the L10nRegistry reset + // when shutting down the addon mananger. This allows us to + // restart under a new locale without a lot of fuss. + let reqLoc = Services.locale.requestedLocales; + Services.locale.requestedLocales = ["es-ES"]; + + prefPromise = promisePrefChanged("default.html"); + await AddonTestUtils.promiseShutdownManager({ clearL10nRegistry: false }); + await AddonTestUtils.promiseStartupManager(); + await extension.awaitStartup(); + await prefPromise; + + Assert.equal( + HomePage.get(), + `moz-extension://${extension.uuid}/default.html`, + "Should have overridden the new homepage" + ); + + await extension.unload(); + + Services.locale.requestedLocales = reqLoc; +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js new file mode 100644 index 0000000000..aac00a8023 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js @@ -0,0 +1,794 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + HomePage: "resource:///modules/HomePage.sys.mjs", + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); + +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "42" +); + +// Similar to TestUtils.topicObserved, but returns a deferred promise that +// can be resolved +function topicObservable(topic, checkFn) { + let deferred = Promise.withResolvers(); + function observer(subject, topic, data) { + try { + if (checkFn && !checkFn(subject, data)) { + return; + } + deferred.resolve([subject, data]); + } catch (ex) { + deferred.reject(ex); + } + } + deferred.promise.finally(() => { + Services.obs.removeObserver(observer, topic); + checkFn = null; + }); + Services.obs.addObserver(observer, topic); + + return deferred; +} + +async function setupRemoteSettings() { + const settings = await RemoteSettings("hijack-blocklists"); + sinon.stub(settings, "get").returns([ + { + id: "homepage-urls", + matches: ["ignore=me"], + _status: "synced", + }, + ]); +} + +function promisePrefChanged(expectedValue) { + return TestUtils.waitForPrefChange("browser.startup.homepage", value => + value.endsWith(expectedValue) + ); +} + +add_task(async function setup() { + await AddonTestUtils.promiseStartupManager(); + await setupRemoteSettings(); +}); + +add_task(async function test_overrides_update_removal() { + /* This tests the scenario where the manifest key for homepage and/or + * search_provider are removed between updates and therefore the + * settings are expected to revert. It also tests that an extension + * can make a builtin extension the default search without user + * interaction. */ + + const EXTENSION_ID = "test_overrides_update@tests.mozilla.org"; + const HOMEPAGE_URI = "webext-homepage-1.html"; + + let extensionInfo = { + useAddonManager: "permanent", + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + chrome_settings_overrides: { + homepage: HOMEPAGE_URI, + search_provider: { + name: "DuckDuckGo", + search_url: "https://example.com/?q={searchTerms}", + is_default: true, + }, + }, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionInfo); + + let defaultHomepageURL = HomePage.get(); + let defaultEngineName = (await Services.search.getDefault()).name; + Assert.notStrictEqual( + defaultEngineName, + "DuckDuckGo", + "Default engine is not DuckDuckGo." + ); + + let prefPromise = promisePrefChanged(HOMEPAGE_URI); + + // When an addon is installed that overrides an app-provided engine (builtin) + // that is the default, we do not prompt for default. + let deferredPrompt = topicObservable( + "webextension-defaultsearch-prompt", + (subject, message) => { + if (subject.wrappedJSObject.id == extension.id) { + ok(false, "default override should not prompt"); + } + } + ); + + await Promise.race([extension.startup(), deferredPrompt.promise]); + deferredPrompt.resolve(); + await AddonTestUtils.waitForSearchProviderStartup(extension); + await prefPromise; + + equal( + extension.version, + "1.0", + "The installed addon has the expected version." + ); + ok( + HomePage.get().endsWith(HOMEPAGE_URI), + "Home page url is overridden by the extension." + ); + equal( + (await Services.search.getDefault()).name, + "DuckDuckGo", + "Builtin default engine was set default by extension" + ); + + extensionInfo.manifest = { + version: "2.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }; + + prefPromise = promisePrefChanged(defaultHomepageURL); + await extension.upgrade(extensionInfo); + await prefPromise; + + equal( + extension.version, + "2.0", + "The updated addon has the expected version." + ); + equal( + HomePage.get(), + defaultHomepageURL, + "Home page url reverted to the default after update." + ); + equal( + (await Services.search.getDefault()).name, + defaultEngineName, + "Default engine reverted to the default after update." + ); + + await extension.unload(); +}); + +add_task(async function test_overrides_update_adding() { + /* This tests the scenario where an addon adds support for + * a homepage or search service when upgrading. Neither + * should override existing entries for those when added + * in an upgrade. Also, a search_provider being added + * with is_default should not prompt the user or override + * the current default engine. */ + + const EXTENSION_ID = "test_overrides_update@tests.mozilla.org"; + const HOMEPAGE_URI = "webext-homepage-1.html"; + + let extensionInfo = { + useAddonManager: "permanent", + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionInfo); + + let defaultHomepageURL = HomePage.get(); + let defaultEngineName = (await Services.search.getDefault()).name; + Assert.notStrictEqual( + defaultEngineName, + "DuckDuckGo", + "Home page url is not DuckDuckGo." + ); + + await extension.startup(); + + equal( + extension.version, + "1.0", + "The installed addon has the expected version." + ); + equal( + HomePage.get(), + defaultHomepageURL, + "Home page url is the default after startup." + ); + equal( + (await Services.search.getDefault()).name, + defaultEngineName, + "Default engine is the default after startup." + ); + + extensionInfo.manifest = { + version: "2.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + chrome_settings_overrides: { + homepage: HOMEPAGE_URI, + search_provider: { + name: "DuckDuckGo", + search_url: "https://example.com/?q={searchTerms}", + is_default: true, + }, + }, + }; + + let prefPromise = promisePrefChanged(HOMEPAGE_URI); + + let deferredUpgradePrompt = topicObservable( + "webextension-defaultsearch-prompt", + (subject, message) => { + if (subject.wrappedJSObject.id == extension.id) { + ok(false, "should not prompt on update"); + } + } + ); + + await Promise.race([ + extension.upgrade(extensionInfo), + deferredUpgradePrompt.promise, + ]); + deferredUpgradePrompt.resolve(); + await AddonTestUtils.waitForSearchProviderStartup(extension); + await prefPromise; + + equal( + extension.version, + "2.0", + "The updated addon has the expected version." + ); + ok( + HomePage.get().endsWith(HOMEPAGE_URI), + "Home page url is overridden by the extension during upgrade." + ); + // An upgraded extension adding a search engine cannot override + // the default engine. + equal( + (await Services.search.getDefault()).name, + defaultEngineName, + "Default engine is still the default after startup." + ); + + await extension.unload(); +}); + +add_task(async function test_overrides_update_homepage_change() { + /* This tests the scenario where an addon changes + * a homepage url when upgrading. */ + + const EXTENSION_ID = "test_overrides_update@tests.mozilla.org"; + const HOMEPAGE_URI = "webext-homepage-1.html"; + const HOMEPAGE_URI_2 = "webext-homepage-2.html"; + + let extensionInfo = { + useAddonManager: "permanent", + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + chrome_settings_overrides: { + homepage: HOMEPAGE_URI, + }, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionInfo); + + let prefPromise = promisePrefChanged(HOMEPAGE_URI); + await extension.startup(); + await prefPromise; + + equal( + extension.version, + "1.0", + "The installed addon has the expected version." + ); + ok( + HomePage.get().endsWith(HOMEPAGE_URI), + "Home page url is the extension url after startup." + ); + + extensionInfo.manifest = { + version: "2.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + chrome_settings_overrides: { + homepage: HOMEPAGE_URI_2, + }, + }; + + prefPromise = promisePrefChanged(HOMEPAGE_URI_2); + await extension.upgrade(extensionInfo); + await prefPromise; + + equal( + extension.version, + "2.0", + "The updated addon has the expected version." + ); + ok( + HomePage.get().endsWith(HOMEPAGE_URI_2), + "Home page url is by the extension after upgrade." + ); + + await extension.unload(); +}); + +async function withHandlingDefaultSearchPrompt({ extensionId, respond }, cb) { + const promptResponseHandled = TestUtils.topicObserved( + "webextension-defaultsearch-prompt-response" + ); + const prompted = TestUtils.topicObserved( + "webextension-defaultsearch-prompt", + (subject, message) => { + if (subject.wrappedJSObject.id == extensionId) { + return subject.wrappedJSObject.respond(respond); + } + } + ); + + await Promise.all([cb(), prompted, promptResponseHandled]); +} + +async function assertUpdateDoNotPrompt(extension, updateExtensionInfo) { + let deferredUpgradePrompt = topicObservable( + "webextension-defaultsearch-prompt", + (subject, message) => { + if (subject.wrappedJSObject.id == extension.id) { + ok(false, "should not prompt on update"); + } + } + ); + + await Promise.race([ + extension.upgrade(updateExtensionInfo), + deferredUpgradePrompt.promise, + ]); + deferredUpgradePrompt.resolve(); + + await AddonTestUtils.waitForSearchProviderStartup(extension); + + equal( + extension.version, + updateExtensionInfo.manifest.version, + "The updated addon has the expected version." + ); +} + +add_task(async function test_default_search_prompts() { + /* This tests the scenario where an addon did not gain + * default search during install, and later upgrades. + * The addon should not gain default in updates. + * If the addon is disabled, it should prompt again when + * enabled. + */ + + const EXTENSION_ID = "test_default_update@tests.mozilla.org"; + + let extensionInfo = { + useAddonManager: "permanent", + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + chrome_settings_overrides: { + search_provider: { + name: "Example", + search_url: "https://example.com/?q={searchTerms}", + is_default: true, + }, + }, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionInfo); + + let defaultEngineName = (await Services.search.getDefault()).name; + Assert.notStrictEqual(defaultEngineName, "Example", "Search is not Example."); + + // Mock a response from the default search prompt where we + // say no to setting this as the default when installing. + await withHandlingDefaultSearchPrompt( + { extensionId: EXTENSION_ID, respond: false }, + () => extension.startup() + ); + + equal( + extension.version, + "1.0", + "The installed addon has the expected version." + ); + equal( + (await Services.search.getDefault()).name, + defaultEngineName, + "Default engine is the default after startup." + ); + + info( + "Verify that updating the extension does not prompt and does not take over the default engine" + ); + + extensionInfo.manifest.version = "2.0"; + await assertUpdateDoNotPrompt(extension, extensionInfo); + equal( + (await Services.search.getDefault()).name, + defaultEngineName, + "Default engine is still the default after update." + ); + + info("Verify that disable/enable the extension does prompt the user"); + + let addon = await AddonManager.getAddonByID(EXTENSION_ID); + + await withHandlingDefaultSearchPrompt( + { extensionId: EXTENSION_ID, respond: false }, + async () => { + await addon.disable(); + await addon.enable(); + } + ); + + // we still said no. + equal( + (await Services.search.getDefault()).name, + defaultEngineName, + "Default engine is the default after being disabling/enabling." + ); + + await extension.unload(); +}); + +async function test_default_search_on_updating_addons_installed_before_bug1757760({ + builtinAsInitialDefault, +}) { + /* This tests covers a scenario similar to the previous test but with an extension-settings.json file + content like the one that would be available in the profile if the add-on was installed on firefox + versions that didn't include the changes from Bug 1757760 (See Bug 1767550). + */ + + const EXTENSION_ID = `test_old_addon@tests.mozilla.org`; + const EXTENSION_ID2 = `test_old_addon2@tests.mozilla.org`; + + const extensionInfo = { + useAddonManager: "permanent", + manifest: { + version: "1.1", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + chrome_settings_overrides: { + search_provider: { + name: "Test SearchEngine", + search_url: "https://example.com/?q={searchTerms}", + is_default: true, + }, + }, + }, + }; + + const extensionInfo2 = { + useAddonManager: "permanent", + manifest: { + version: "1.2", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID2, + }, + }, + chrome_settings_overrides: { + search_provider: { + name: "Test SearchEngine2", + search_url: "https://example.com/?q={searchTerms}", + is_default: true, + }, + }, + }, + }; + + const { ExtensionSettingsStore } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionSettingsStore.sys.mjs" + ); + + async function assertExtensionSettingsStore( + extensionInfo, + expectedLevelOfControl + ) { + const { id } = extensionInfo.manifest.browser_specific_settings.gecko; + info(`Asserting ExtensionSettingsStore for ${id}`); + const item = ExtensionSettingsStore.getSetting( + "default_search", + "defaultSearch", + id + ); + equal( + item.value, + extensionInfo.manifest.chrome_settings_overrides.search_provider.name, + "Got the expected item returned by ExtensionSettingsStore.getSetting" + ); + const control = await ExtensionSettingsStore.getLevelOfControl( + id, + "default_search", + "defaultSearch" + ); + equal( + control, + expectedLevelOfControl, + `Got expected levelOfControl for ${id}` + ); + } + + info("Install test extensions without opt-in to the related search engines"); + + let extension = ExtensionTestUtils.loadExtension(extensionInfo); + let extension2 = ExtensionTestUtils.loadExtension(extensionInfo2); + + // Mock a response from the default search prompt where we + // say no to setting this as the default when installing. + await withHandlingDefaultSearchPrompt( + { extensionId: EXTENSION_ID, respond: false }, + () => extension.startup() + ); + + equal( + extension.version, + "1.1", + "first installed addon has the expected version." + ); + + // Mock a response from the default search prompt where we + // say no to setting this as the default when installing. + await withHandlingDefaultSearchPrompt( + { extensionId: EXTENSION_ID2, respond: false }, + () => extension2.startup() + ); + + equal( + extension2.version, + "1.2", + "second installed addon has the expected version." + ); + + info("Setup preconditions (set the initial default search engine)"); + + // Sanity check to be sure the initial engine expected as precondition + // for the scenario covered by the current test case. + let initialEngine; + if (builtinAsInitialDefault) { + initialEngine = Services.search.appDefaultEngine; + } else { + initialEngine = Services.search.getEngineByName( + extensionInfo.manifest.chrome_settings_overrides.search_provider.name + ); + } + await Services.search.setDefault( + initialEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + let defaultEngineName = (await Services.search.getDefault()).name; + Assert.equal( + defaultEngineName, + initialEngine.name, + `initial default search engine expected to be ${ + builtinAsInitialDefault ? "app-provided" : EXTENSION_ID + }` + ); + Assert.notEqual( + defaultEngineName, + extensionInfo2.manifest.chrome_settings_overrides.search_provider.name, + "initial default search engine name should not be the same as the second extension search_provider" + ); + + equal( + (await Services.search.getDefault()).name, + initialEngine.name, + `Default engine should still be set to the ${ + builtinAsInitialDefault ? "app-provided" : EXTENSION_ID + }.` + ); + + // Mock an update from settings stored as in an older Firefox version where Bug 1757760 was not landed yet. + info( + "Setup preconditions (inject mock extension-settings.json data and assert on the expected setting and levelOfControl)" + ); + + let addon = await AddonManager.getAddonByID(EXTENSION_ID); + let addon2 = await AddonManager.getAddonByID(EXTENSION_ID2); + + const extensionSettingsData = { + version: 2, + url_overrides: {}, + prefs: {}, + homepageNotification: {}, + tabHideNotification: {}, + default_search: { + defaultSearch: { + initialValue: Services.search.appDefaultEngine.name, + precedenceList: [ + { + id: EXTENSION_ID2, + // The install dates are used in ExtensionSettingsStore.getLevelOfControl + // and to recreate the expected preconditions the last extension installed + // should have a installDate timestamp > then the first one. + installDate: addon2.installDate.getTime() + 1000, + value: + extensionInfo2.manifest.chrome_settings_overrides.search_provider + .name, + // When an addon with a default search engine override is installed in Firefox versions + // without the changes landed from Bug 1757760, `enabled` will be set to true in all cases + // (Prompt never answered, or when No or Yes is selected by the user). + enabled: true, + }, + { + id: EXTENSION_ID, + installDate: addon.installDate.getTime(), + value: + extensionInfo.manifest.chrome_settings_overrides.search_provider + .name, + enabled: true, + }, + ], + }, + }, + newTabNotification: {}, + commands: {}, + }; + + const file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("extension-settings.json"); + + info(`writing mock settings data into ${file.path}`); + await IOUtils.writeJSON(file.path, extensionSettingsData); + await ExtensionSettingsStore._reloadFile(false); + + equal( + (await Services.search.getDefault()).name, + initialEngine.name, + "Default engine is still set to the initial one." + ); + + // The following assertions verify that the migration applied from ExtensionSettingsStore + // fixed the inconsistent state and kept the search engine unchanged. + // + // - With the fixed settings we expect both to be resolved to "controllable_by_this_extension". + // - Without the fix applied during the migration the levelOfControl resolved would be: + // - for the last installed: "controlled_by_this_extension" + // - for the first installed: "controlled_by_other_extensions" + await assertExtensionSettingsStore( + extensionInfo2, + "controlled_by_this_extension" + ); + await assertExtensionSettingsStore( + extensionInfo, + "controlled_by_other_extensions" + ); + + info( + "Verify that updating the extension does not prompt and does not take over the default engine" + ); + + extensionInfo2.manifest.version = "2.2"; + await assertUpdateDoNotPrompt(extension2, extensionInfo2); + + extensionInfo.manifest.version = "2.1"; + await assertUpdateDoNotPrompt(extension, extensionInfo); + + equal( + (await Services.search.getDefault()).name, + initialEngine.name, + "Default engine is still the same after updating both the test extensions." + ); + + // After both the extensions have been updated and their inconsistent state + // updated internally, both extensions should have levelOfControl "controllable_*". + await assertExtensionSettingsStore( + extensionInfo2, + "controllable_by_this_extension" + ); + await assertExtensionSettingsStore( + extensionInfo, + // We expect levelOfControl to be controlled_by_this_extension if the test case + // is expecting the third party extension to stay set as default. + builtinAsInitialDefault + ? "controllable_by_this_extension" + : "controlled_by_this_extension" + ); + + info("Verify that disable/enable the extension does prompt the user"); + + await withHandlingDefaultSearchPrompt( + { extensionId: EXTENSION_ID2, respond: false }, + async () => { + await addon2.disable(); + await addon2.enable(); + } + ); + + // we said no. + equal( + (await Services.search.getDefault()).name, + initialEngine.name, + `Default engine should still be the same after disabling/enabling ${EXTENSION_ID2}.` + ); + + await withHandlingDefaultSearchPrompt( + { extensionId: EXTENSION_ID, respond: false }, + async () => { + await addon.disable(); + await addon.enable(); + } + ); + + // we said no. + equal( + (await Services.search.getDefault()).name, + Services.search.appDefaultEngine.name, + `Default engine should be set to the app default after disabling/enabling ${EXTENSION_ID}.` + ); + + await withHandlingDefaultSearchPrompt( + { extensionId: EXTENSION_ID, respond: true }, + async () => { + await addon.disable(); + await addon.enable(); + } + ); + + // we responded yes. + equal( + (await Services.search.getDefault()).name, + extensionInfo.manifest.chrome_settings_overrides.search_provider.name, + "Default engine should be set to the one opted-in from the last prompt." + ); + + await extension.unload(); + await extension2.unload(); +} + +add_task(function test_builtin_default_search_after_updating_old_addons() { + return test_default_search_on_updating_addons_installed_before_bug1757760({ + builtinAsInitialDefault: true, + }); +}); + +add_task(function test_third_party_default_search_after_updating_old_addons() { + return test_default_search_on_updating_addons_installed_before_bug1757760({ + builtinAsInitialDefault: false, + }); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_distribution_popup.js b/browser/components/extensions/test/xpcshell/test_ext_distribution_popup.js new file mode 100644 index 0000000000..030e0b27be --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_distribution_popup.js @@ -0,0 +1,56 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + ExtensionControlledPopup: + "resource:///modules/ExtensionControlledPopup.sys.mjs", + ExtensionSettingsStore: + "resource://gre/modules/ExtensionSettingsStore.sys.mjs", +}); + +/* + * This function is a unit test for distributions disabling the ExtensionControlledPopup. + */ +add_task(async function testDistributionPopup() { + let distExtId = "ext-distribution@mochi.test"; + Services.prefs.setCharPref( + `extensions.installedDistroAddon.${distExtId}`, + true + ); + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id: distExtId } }, + name: "Ext Distribution", + }, + }); + + let userExtId = "ext-user@mochi.test"; + let userExtension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id: userExtId } }, + name: "Ext User Installed", + }, + }); + + await extension.startup(); + await userExtension.startup(); + await ExtensionSettingsStore.initialize(); + + let confirmedType = "extension-controlled-confirmed"; + equal( + new ExtensionControlledPopup({ confirmedType }).userHasConfirmed(distExtId), + true, + "The popup has been disabled." + ); + + equal( + new ExtensionControlledPopup({ confirmedType }).userHasConfirmed(userExtId), + false, + "The popup has not been disabled." + ); + + await extension.unload(); + await userExtension.unload(); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_history.js b/browser/components/extensions/test/xpcshell/test_ext_history.js new file mode 100644 index 0000000000..c0f6c39be7 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_history.js @@ -0,0 +1,864 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + ExtensionCommon: "resource://gre/modules/ExtensionCommon.sys.mjs", + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", +}); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "43" +); + +add_task(async function test_delete() { + function background() { + let historyClearedCount = 0; + let removedUrls = []; + + browser.history.onVisitRemoved.addListener(data => { + if (data.allHistory) { + historyClearedCount++; + browser.test.assertEq( + 0, + data.urls.length, + "onVisitRemoved received an empty urls array" + ); + } else { + removedUrls.push(...data.urls); + } + }); + + browser.test.onMessage.addListener((msg, arg) => { + if (msg === "delete-url") { + browser.history.deleteUrl({ url: arg }).then(result => { + browser.test.assertEq( + undefined, + result, + "browser.history.deleteUrl returns nothing" + ); + browser.test.sendMessage("url-deleted"); + }); + } else if (msg === "delete-range") { + browser.history.deleteRange(arg).then(result => { + browser.test.assertEq( + undefined, + result, + "browser.history.deleteRange returns nothing" + ); + browser.test.sendMessage("range-deleted", removedUrls); + }); + } else if (msg === "delete-all") { + browser.history.deleteAll().then(result => { + browser.test.assertEq( + undefined, + result, + "browser.history.deleteAll returns nothing" + ); + browser.test.sendMessage("history-cleared", [ + historyClearedCount, + removedUrls, + ]); + }); + } + }); + + browser.test.sendMessage("ready"); + } + + const BASE_URL = "http://mozilla.com/test_history/"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["history"], + }, + background: `(${background})()`, + }); + + await extension.startup(); + await extension.awaitMessage("ready"); + await PlacesUtils.history.clear(); + + let historyClearedCount; + let visits = []; + let visitDate = new Date(1999, 9, 9, 9, 9).getTime(); + + function pushVisit(subvisits) { + visitDate += 1000; + subvisits.push({ date: new Date(visitDate) }); + } + + // Add 5 visits for one uri and 3 visits for 3 others + for (let i = 0; i < 4; ++i) { + let visit = { + url: `${BASE_URL}${i}`, + title: "visit " + i, + visits: [], + }; + if (i === 0) { + for (let j = 0; j < 5; ++j) { + pushVisit(visit.visits); + } + } else { + pushVisit(visit.visits); + } + visits.push(visit); + } + + await PlacesUtils.history.insertMany(visits); + equal( + await PlacesTestUtils.visitsInDB(visits[0].url), + 5, + "5 visits for uri found in history database" + ); + + let testUrl = visits[2].url; + ok( + await PlacesTestUtils.isPageInDB(testUrl), + "expected url found in history database" + ); + + extension.sendMessage("delete-url", testUrl); + await extension.awaitMessage("url-deleted"); + equal( + await PlacesTestUtils.isPageInDB(testUrl), + false, + "expected url not found in history database" + ); + + // delete 3 of the 5 visits for url 1 + let filter = { + startTime: visits[0].visits[0].date, + endTime: visits[0].visits[2].date, + }; + + extension.sendMessage("delete-range", filter); + let removedUrls = await extension.awaitMessage("range-deleted"); + ok( + !removedUrls.includes(visits[0].url), + `${visits[0].url} not received by onVisitRemoved` + ); + ok( + await PlacesTestUtils.isPageInDB(visits[0].url), + "expected uri found in history database" + ); + equal( + await PlacesTestUtils.visitsInDB(visits[0].url), + 2, + "2 visits for uri found in history database" + ); + ok( + await PlacesTestUtils.isPageInDB(visits[1].url), + "expected uri found in history database" + ); + equal( + await PlacesTestUtils.visitsInDB(visits[1].url), + 1, + "1 visit for uri found in history database" + ); + + // delete the rest of the visits for url 1, and the visit for url 2 + filter.startTime = visits[0].visits[0].date; + filter.endTime = visits[1].visits[0].date; + + extension.sendMessage("delete-range", filter); + await extension.awaitMessage("range-deleted"); + + equal( + await PlacesTestUtils.isPageInDB(visits[0].url), + false, + "expected uri not found in history database" + ); + equal( + await PlacesTestUtils.visitsInDB(visits[0].url), + 0, + "0 visits for uri found in history database" + ); + equal( + await PlacesTestUtils.isPageInDB(visits[1].url), + false, + "expected uri not found in history database" + ); + equal( + await PlacesTestUtils.visitsInDB(visits[1].url), + 0, + "0 visits for uri found in history database" + ); + + ok( + await PlacesTestUtils.isPageInDB(visits[3].url), + "expected uri found in history database" + ); + + extension.sendMessage("delete-all"); + [historyClearedCount, removedUrls] = await extension.awaitMessage( + "history-cleared" + ); + equal( + historyClearedCount, + 2, + "onVisitRemoved called for each clearing of history" + ); + equal( + removedUrls.length, + 3, + "onVisitRemoved called the expected number of times" + ); + for (let i = 1; i < 3; ++i) { + let url = visits[i].url; + ok(removedUrls.includes(url), `${url} received by onVisitRemoved`); + } + await extension.unload(); +}); + +const SINGLE_VISIT_URL = "http://example.com/"; +const DOUBLE_VISIT_URL = "http://example.com/2/"; +const MOZILLA_VISIT_URL = "http://mozilla.com/"; +const REFERENCE_DATE = new Date(); +// pages/visits to add via History.insert +const PAGE_INFOS = [ + { + url: SINGLE_VISIT_URL, + title: `test visit for ${SINGLE_VISIT_URL}`, + visits: [{ date: new Date(Number(REFERENCE_DATE) - 1000) }], + }, + { + url: DOUBLE_VISIT_URL, + title: `test visit for ${DOUBLE_VISIT_URL}`, + visits: [ + { date: REFERENCE_DATE }, + { date: new Date(Number(REFERENCE_DATE) - 2000) }, + ], + }, + { + url: MOZILLA_VISIT_URL, + title: `test visit for ${MOZILLA_VISIT_URL}`, + visits: [{ date: new Date(Number(REFERENCE_DATE) - 3000) }], + }, +]; + +add_task(async function test_search() { + function background(BGSCRIPT_REFERENCE_DATE) { + const futureTime = Date.now() + 24 * 60 * 60 * 1000; + + browser.test.onMessage.addListener(msg => { + browser.history + .search({ text: "" }) + .then(results => { + browser.test.sendMessage("empty-search", results); + return browser.history.search({ text: "mozilla.com" }); + }) + .then(results => { + browser.test.sendMessage("text-search", results); + return browser.history.search({ text: "example.com", maxResults: 1 }); + }) + .then(results => { + browser.test.sendMessage("max-results-search", results); + return browser.history.search({ + text: "", + startTime: BGSCRIPT_REFERENCE_DATE - 2000, + endTime: BGSCRIPT_REFERENCE_DATE - 1000, + }); + }) + .then(results => { + browser.test.sendMessage("date-range-search", results); + return browser.history.search({ text: "", startTime: futureTime }); + }) + .then(results => { + browser.test.assertEq( + 0, + results.length, + "no results returned for late start time" + ); + return browser.history.search({ text: "", endTime: 0 }); + }) + .then(results => { + browser.test.assertEq( + 0, + results.length, + "no results returned for early end time" + ); + return browser.history.search({ + text: "", + startTime: Date.now(), + endTime: 0, + }); + }) + .then( + results => { + browser.test.fail( + "history.search rejects with startTime that is after the endTime" + ); + }, + error => { + browser.test.assertEq( + "The startTime cannot be after the endTime", + error.message, + "history.search rejects with startTime that is after the endTime" + ); + } + ) + .then(() => { + browser.test.notifyPass("search"); + }); + }); + + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["history"], + }, + background: `(${background})(${Number(REFERENCE_DATE)})`, + }); + + function findResult(url, results) { + return results.find(r => r.url === url); + } + + function checkResult(results, url, expectedCount) { + let result = findResult(url, results); + notEqual(result, null, `history.search result was found for ${url}`); + equal( + result.visitCount, + expectedCount, + `history.search reports ${expectedCount} visit(s)` + ); + equal( + result.title, + `test visit for ${url}`, + "title for search result is correct" + ); + } + + await extension.startup(); + await extension.awaitMessage("ready"); + await PlacesUtils.history.clear(); + + await PlacesUtils.history.insertMany(PAGE_INFOS); + + extension.sendMessage("check-history"); + + let results = await extension.awaitMessage("empty-search"); + equal(results.length, 3, "history.search with empty text returned 3 results"); + checkResult(results, SINGLE_VISIT_URL, 1); + checkResult(results, DOUBLE_VISIT_URL, 2); + checkResult(results, MOZILLA_VISIT_URL, 1); + + results = await extension.awaitMessage("text-search"); + equal( + results.length, + 1, + "history.search with specific text returned 1 result" + ); + checkResult(results, MOZILLA_VISIT_URL, 1); + + results = await extension.awaitMessage("max-results-search"); + equal(results.length, 1, "history.search with maxResults returned 1 result"); + checkResult(results, DOUBLE_VISIT_URL, 2); + + results = await extension.awaitMessage("date-range-search"); + equal( + results.length, + 2, + "history.search with a date range returned 2 result" + ); + checkResult(results, DOUBLE_VISIT_URL, 2); + checkResult(results, SINGLE_VISIT_URL, 1); + + await extension.awaitFinish("search"); + await extension.unload(); + await PlacesUtils.history.clear(); +}); + +add_task(async function test_add_url() { + function background() { + const TEST_DOMAIN = "http://example.com/"; + + browser.test.onMessage.addListener((msg, testData) => { + let [details, type] = testData; + details.url = details.url || `${TEST_DOMAIN}${type}`; + if (msg === "add-url") { + details.title = `Title for ${type}`; + browser.history + .addUrl(details) + .then(() => { + return browser.history.search({ text: details.url }); + }) + .then(results => { + browser.test.assertEq( + 1, + results.length, + "1 result found when searching for added URL" + ); + browser.test.sendMessage("url-added", { + details, + result: results[0], + }); + }); + } else if (msg === "expect-failure") { + let expectedMsg = testData[2]; + browser.history.addUrl(details).then( + () => { + browser.test.fail(`Expected error thrown for ${type}`); + }, + error => { + browser.test.assertTrue( + error.message.includes(expectedMsg), + `"Expected error thrown when trying to add a URL with ${type}` + ); + browser.test.sendMessage("add-failed"); + } + ); + } + }); + + browser.test.sendMessage("ready"); + } + + let addTestData = [ + [{}, "default"], + [{ visitTime: new Date() }, "with_date"], + [{ visitTime: Date.now() }, "with_ms_number"], + [{ visitTime: new Date().toISOString() }, "with_iso_string"], + [{ transition: "typed" }, "valid_transition"], + ]; + + let failTestData = [ + [ + { transition: "generated" }, + "an invalid transition", + "|generated| is not a supported transition for history", + ], + [{ visitTime: Date.now() + 1000000 }, "a future date", "Invalid value"], + [{ url: "about.config" }, "an invalid url", "Invalid value"], + ]; + + async function checkUrl(results) { + ok( + await PlacesTestUtils.isPageInDB(results.details.url), + `${results.details.url} found in history database` + ); + ok( + PlacesUtils.isValidGuid(results.result.id), + "URL was added with a valid id" + ); + equal( + results.result.title, + results.details.title, + "URL was added with the correct title" + ); + if (results.details.visitTime) { + equal( + results.result.lastVisitTime, + Number(ExtensionCommon.normalizeTime(results.details.visitTime)), + "URL was added with the correct date" + ); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["history"], + }, + background: `(${background})()`, + }); + + await PlacesUtils.history.clear(); + await extension.startup(); + await extension.awaitMessage("ready"); + + for (let data of addTestData) { + extension.sendMessage("add-url", data); + let results = await extension.awaitMessage("url-added"); + await checkUrl(results); + } + + for (let data of failTestData) { + extension.sendMessage("expect-failure", data); + await extension.awaitMessage("add-failed"); + } + + await extension.unload(); +}); + +add_task(async function test_get_visits() { + async function background() { + const TEST_DOMAIN = "http://example.com/"; + const FIRST_DATE = Date.now(); + const INITIAL_DETAILS = { + url: TEST_DOMAIN, + visitTime: FIRST_DATE, + transition: "link", + }; + + let visitIds = new Set(); + + async function checkVisit(visit, expected) { + visitIds.add(visit.visitId); + browser.test.assertEq( + expected.visitTime, + visit.visitTime, + "visit has the correct visitTime" + ); + browser.test.assertEq( + expected.transition, + visit.transition, + "visit has the correct transition" + ); + let results = await browser.history.search({ text: expected.url }); + // all results will have the same id, so we only need to use the first one + browser.test.assertEq( + results[0].id, + visit.id, + "visit has the correct id" + ); + } + + let details = Object.assign({}, INITIAL_DETAILS); + + await browser.history.addUrl(details); + let results = await browser.history.getVisits({ url: details.url }); + + browser.test.assertEq( + 1, + results.length, + "the expected number of visits were returned" + ); + await checkVisit(results[0], details); + + details.url = `${TEST_DOMAIN}/1/`; + await browser.history.addUrl(details); + + results = await browser.history.getVisits({ url: details.url }); + browser.test.assertEq( + 1, + results.length, + "the expected number of visits were returned" + ); + await checkVisit(results[0], details); + + details.visitTime = FIRST_DATE - 1000; + details.transition = "typed"; + await browser.history.addUrl(details); + results = await browser.history.getVisits({ url: details.url }); + + browser.test.assertEq( + 2, + results.length, + "the expected number of visits were returned" + ); + await checkVisit(results[0], INITIAL_DETAILS); + await checkVisit(results[1], details); + browser.test.assertEq(3, visitIds.size, "each visit has a unique visitId"); + await browser.test.notifyPass("get-visits"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["history"], + }, + background: `(${background})()`, + }); + + await PlacesUtils.history.clear(); + await extension.startup(); + + await extension.awaitFinish("get-visits"); + await extension.unload(); +}); + +add_task(async function test_transition_types() { + const VISIT_URL_PREFIX = "http://example.com/"; + const TRANSITIONS = [ + ["link", Ci.nsINavHistoryService.TRANSITION_LINK], + ["typed", Ci.nsINavHistoryService.TRANSITION_TYPED], + ["auto_bookmark", Ci.nsINavHistoryService.TRANSITION_BOOKMARK], + // Only session history contains TRANSITION_EMBED visits, + // So global history query cannot find them. + // ["auto_subframe", Ci.nsINavHistoryService.TRANSITION_EMBED], + // Redirects are not correctly tested here because History + // will not make redirect entries hidden. + ["link", Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT], + ["link", Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY], + ["link", Ci.nsINavHistoryService.TRANSITION_DOWNLOAD], + ["manual_subframe", Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK], + ["reload", Ci.nsINavHistoryService.TRANSITION_RELOAD], + ]; + + // pages/visits to add via History.insertMany + let pageInfos = []; + let visitDate = new Date(1999, 9, 9, 9, 9).getTime(); + for (let [, transitionType] of TRANSITIONS) { + pageInfos.push({ + url: VISIT_URL_PREFIX + transitionType + "/", + visits: [ + { transition: transitionType, date: new Date((visitDate -= 1000)) }, + ], + }); + } + + function background() { + browser.test.onMessage.addListener(async (msg, url) => { + switch (msg) { + case "search": { + let results = await browser.history.search({ + text: "", + startTime: new Date(0), + }); + browser.test.sendMessage("search-result", results); + break; + } + case "get-visits": { + let results = await browser.history.getVisits({ url }); + browser.test.sendMessage("get-visits-result", results); + break; + } + } + }); + + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["history"], + }, + background, + }); + + await PlacesUtils.history.clear(); + await extension.startup(); + await extension.awaitMessage("ready"); + + await PlacesUtils.history.insertMany(pageInfos); + + extension.sendMessage("search"); + let results = await extension.awaitMessage("search-result"); + equal( + results.length, + pageInfos.length, + "search returned expected length of results" + ); + for (let i = 0; i < pageInfos.length; ++i) { + equal(results[i].url, pageInfos[i].url, "search returned the expected url"); + + extension.sendMessage("get-visits", pageInfos[i].url); + let visits = await extension.awaitMessage("get-visits-result"); + equal(visits.length, 1, "getVisits returned expected length of visits"); + equal( + visits[0].transition, + TRANSITIONS[i][0], + "getVisits returned the expected transition" + ); + } + + await extension.unload(); +}); + +add_task(async function test_on_visited() { + const SINGLE_VISIT_URL = "http://example.com/1/"; + const DOUBLE_VISIT_URL = "http://example.com/2/"; + let visitDate = new Date(1999, 9, 9, 9, 9).getTime(); + + // pages/visits to add via History.insertMany + const PAGE_INFOS = [ + { + url: SINGLE_VISIT_URL, + title: `visit to ${SINGLE_VISIT_URL}`, + visits: [{ date: new Date(visitDate) }], + }, + { + url: DOUBLE_VISIT_URL, + title: `visit to ${DOUBLE_VISIT_URL}`, + visits: [ + { date: new Date((visitDate += 1000)) }, + { date: new Date((visitDate += 1000)) }, + ], + }, + { + url: SINGLE_VISIT_URL, + title: "Title Changed", + visits: [{ date: new Date(visitDate) }], + }, + ]; + + function background() { + let onVisitedData = []; + + browser.history.onVisited.addListener(data => { + if (data.url.includes("moz-extension")) { + return; + } + onVisitedData.push(data); + if (onVisitedData.length == 4) { + browser.test.sendMessage("on-visited-data", onVisitedData); + } + }); + + // Verifying onTitleChange Event along with onVisited event + browser.history.onTitleChanged.addListener(data => { + browser.test.sendMessage("on-title-changed-data", data); + }); + + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["history"], + }, + background: `(${background})()`, + }); + + await PlacesUtils.history.clear(); + await extension.startup(); + await extension.awaitMessage("ready"); + + await PlacesUtils.history.insertMany(PAGE_INFOS); + + let onVisitedData = await extension.awaitMessage("on-visited-data"); + + function checkOnVisitedData(index, expected) { + let onVisited = onVisitedData[index]; + ok(PlacesUtils.isValidGuid(onVisited.id), "onVisited received a valid id"); + equal(onVisited.url, expected.url, "onVisited received the expected url"); + equal( + onVisited.title, + expected.title, + "onVisited received the expected title" + ); + equal( + onVisited.lastVisitTime, + expected.time, + "onVisited received the expected time" + ); + equal( + onVisited.visitCount, + expected.visitCount, + "onVisited received the expected visitCount" + ); + } + + let expected = { + url: PAGE_INFOS[0].url, + title: PAGE_INFOS[0].title, + time: PAGE_INFOS[0].visits[0].date.getTime(), + visitCount: 1, + }; + checkOnVisitedData(0, expected); + + expected.url = PAGE_INFOS[1].url; + expected.title = PAGE_INFOS[1].title; + expected.time = PAGE_INFOS[1].visits[0].date.getTime(); + checkOnVisitedData(1, expected); + + expected.time = PAGE_INFOS[1].visits[1].date.getTime(); + expected.visitCount = 2; + checkOnVisitedData(2, expected); + + expected.url = PAGE_INFOS[2].url; + expected.title = PAGE_INFOS[2].title; + expected.time = PAGE_INFOS[2].visits[0].date.getTime(); + expected.visitCount = 2; + checkOnVisitedData(3, expected); + + let onTitleChangedData = await extension.awaitMessage( + "on-title-changed-data" + ); + Assert.deepEqual( + { + id: onVisitedData[3].id, + url: SINGLE_VISIT_URL, + title: "Title Changed", + }, + onTitleChangedData, + "expected event data for onTitleChanged" + ); + + await extension.unload(); +}); + +add_task( + { + pref_set: [["extensions.eventPages.enabled", true]], + }, + async function test_history_event_page() { + await AddonTestUtils.promiseStartupManager(); + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + browser_specific_settings: { gecko: { id: "eventpage@history" } }, + permissions: ["history"], + background: { persistent: false }, + }, + background() { + browser.history.onVisited.addListener(() => { + browser.test.sendMessage("onVisited"); + }); + browser.history.onVisitRemoved.addListener(() => { + browser.test.sendMessage("onVisitRemoved"); + }); + browser.history.onTitleChanged.addListener(() => {}); + browser.test.sendMessage("ready"); + }, + }); + + const EVENTS = ["onVisited", "onVisitRemoved", "onTitleChanged"]; + await PlacesUtils.history.clear(); + + await extension.startup(); + await extension.awaitMessage("ready"); + for (let event of EVENTS) { + assertPersistentListeners(extension, "history", event, { + primed: false, + }); + } + + // test events waken background + await extension.terminateBackground(); + for (let event of EVENTS) { + assertPersistentListeners(extension, "history", event, { + primed: true, + }); + } + + await PlacesUtils.history.insertMany(PAGE_INFOS); + + await extension.awaitMessage("ready"); + await extension.awaitMessage("onVisited"); + ok(true, "persistent event woke background"); + for (let event of EVENTS) { + assertPersistentListeners(extension, "history", event, { + primed: false, + }); + } + + await AddonTestUtils.promiseRestartManager(); + await extension.awaitStartup(); + + for (let event of EVENTS) { + assertPersistentListeners(extension, "history", event, { + primed: true, + }); + } + + await PlacesUtils.history.clear(); + await extension.awaitMessage("ready"); + await extension.awaitMessage("onVisitRemoved"); + + await extension.unload(); + await AddonTestUtils.promiseShutdownManager(); + } +); diff --git a/browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js b/browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js new file mode 100644 index 0000000000..2d2bccc1e2 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js @@ -0,0 +1,134 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { HomePage } = ChromeUtils.importESModule( + "resource:///modules/HomePage.sys.mjs" +); +const { ExtensionPermissions } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionPermissions.sys.mjs" +); + +const { createAppInfo, promiseShutdownManager, promiseStartupManager } = + AddonTestUtils; + +const EXTENSION_ID = "test_overrides@tests.mozilla.org"; +const HOMEPAGE_EXTENSION_CONTROLLED = + "browser.startup.homepage_override.extensionControlled"; +const HOMEPAGE_PRIVATE_ALLOWED = + "browser.startup.homepage_override.privateAllowed"; +const HOMEPAGE_URL_PREF = "browser.startup.homepage"; +const HOMEPAGE_URI = "webext-homepage-1.html"; + +Services.prefs.setBoolPref("browser.privatebrowsing.autostart", true); + +AddonTestUtils.init(this); +AddonTestUtils.usePrivilegedSignatures = false; +AddonTestUtils.overrideCertDB(); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +function promisePrefChange(pref) { + return new Promise((resolve, reject) => { + Services.prefs.addObserver(pref, function observer() { + Services.prefs.removeObserver(pref, observer); + resolve(arguments); + }); + }); +} + +let defaultHomepageURL; + +function verifyPrefSettings(controlled, allowed) { + equal( + Services.prefs.getBoolPref(HOMEPAGE_EXTENSION_CONTROLLED, false), + controlled, + "homepage extension controlled" + ); + equal( + Services.prefs.getBoolPref(HOMEPAGE_PRIVATE_ALLOWED, false), + allowed, + "homepage private permission after permission change" + ); + + if (controlled && allowed) { + ok( + HomePage.get().endsWith(HOMEPAGE_URI), + "Home page url is overridden by the extension" + ); + } else { + equal(HomePage.get(), defaultHomepageURL, "Home page url is default."); + } +} + +async function promiseUpdatePrivatePermission(allowed, extension) { + info(`update private allowed permission`); + await Promise.all([ + promisePrefChange(HOMEPAGE_PRIVATE_ALLOWED), + ExtensionPermissions[allowed ? "add" : "remove"]( + extension.id, + { permissions: ["internal:privateBrowsingAllowed"], origins: [] }, + extension + ), + ]); + + verifyPrefSettings(true, allowed); +} + +add_task(async function test_overrides_private() { + await promiseStartupManager(); + + let extensionInfo = { + useAddonManager: "permanent", + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + chrome_settings_overrides: { + homepage: HOMEPAGE_URI, + }, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionInfo); + + defaultHomepageURL = HomePage.get(); + + await extension.startup(); + + verifyPrefSettings(true, false); + + equal(HomePage.get(), defaultHomepageURL, "Home page url is default."); + + info("add permission to extension"); + await promiseUpdatePrivatePermission(true, extension.extension); + info("remove permission from extension"); + await promiseUpdatePrivatePermission(false, extension.extension); + // set back to true to test upgrade removing extension control + info("add permission back to prepare for upgrade test"); + await promiseUpdatePrivatePermission(true, extension.extension); + + extensionInfo.manifest = { + version: "2.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }; + + await Promise.all([ + promisePrefChange(HOMEPAGE_URL_PREF), + extension.upgrade(extensionInfo), + ]); + + verifyPrefSettings(false, false); + + await extension.unload(); + await promiseShutdownManager(); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_manifest.js b/browser/components/extensions/test/xpcshell/test_ext_manifest.js new file mode 100644 index 0000000000..b978172ca2 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_manifest.js @@ -0,0 +1,105 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); + +async function testManifest(manifest, expectedError) { + ExtensionTestUtils.failOnSchemaWarnings(false); + let normalized = await ExtensionTestUtils.normalizeManifest(manifest); + ExtensionTestUtils.failOnSchemaWarnings(true); + + if (expectedError) { + ok( + expectedError.test(normalized.error), + `Should have an error for ${JSON.stringify(manifest)}, got ${ + normalized.error + }` + ); + } else { + ok( + !normalized.error, + `Should not have an error ${JSON.stringify(manifest)}, ${ + normalized.error + }` + ); + } + return normalized.errors; +} + +const all_actions = [ + "action", + "browser_action", + "page_action", + "sidebar_action", +]; + +add_task(async function test_manifest() { + let badpaths = ["", " ", "\t", "http://foo.com/icon.png"]; + for (let path of badpaths) { + for (let action of all_actions) { + let manifest_version = action == "action" ? 3 : 2; + let manifest = { manifest_version }; + manifest[action] = { default_icon: path }; + let error = new RegExp(`Error processing ${action}.default_icon`); + await testManifest(manifest, error); + + manifest[action] = { default_icon: { 16: path } }; + await testManifest(manifest, error); + } + } + + let paths = [ + "icon.png", + "/icon.png", + "./icon.png", + "path to an icon.png", + " icon.png", + ]; + for (let path of paths) { + for (let action of all_actions) { + let manifest_version = action == "action" ? 3 : 2; + let manifest = { manifest_version }; + manifest[action] = { default_icon: path }; + if (action == "sidebar_action") { + // Sidebar requires panel. + manifest[action].default_panel = "foo.html"; + } + await testManifest(manifest); + + manifest[action] = { default_icon: { 16: path } }; + if (action == "sidebar_action") { + manifest[action].default_panel = "foo.html"; + } + await testManifest(manifest); + } + } +}); + +add_task(async function test_action_version() { + let warnings = await testManifest({ + manifest_version: 3, + browser_action: { + default_panel: "foo.html", + }, + }); + Assert.deepEqual( + warnings, + [`Property "browser_action" is unsupported in Manifest Version 3`], + `Manifest v3 with "browser_action" key logs an error.` + ); + + warnings = await testManifest({ + manifest_version: 2, + action: { + default_icon: "", + default_panel: "foo.html", + }, + }); + + Assert.deepEqual( + warnings, + [`Property "action" is unsupported in Manifest Version 2`], + `Manifest v2 with "action" key first warning is clear.` + ); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js b/browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js new file mode 100644 index 0000000000..8196ab0e24 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js @@ -0,0 +1,52 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(async function test_manifest_commands() { + const validShortcuts = [ + "Ctrl+Y", + "MacCtrl+Y", + "Command+Y", + "Alt+Shift+Y", + "Ctrl+Alt+Y", + "F1", + "MediaNextTrack", + ]; + const invalidShortcuts = ["Shift+Y", "Y", "Ctrl+Ctrl+Y", "Ctrl+Command+Y"]; + + async function validateShortcut(shortcut, isValid) { + let normalized = await ExtensionTestUtils.normalizeManifest({ + commands: { + "toggle-feature": { + suggested_key: { default: shortcut }, + description: "Send a 'toggle-feature' event to the extension", + }, + }, + }); + if (isValid) { + ok(!normalized.error, "There should be no manifest errors."); + } else { + let expectedError = + String.raw`Error processing commands.toggle-feature.suggested_key.default: Error: ` + + String.raw`Value "${shortcut}" must consist of ` + + String.raw`either a combination of one or two modifiers, including ` + + String.raw`a mandatory primary modifier and a key, separated by '+', ` + + String.raw`or a media key. For details see: ` + + String.raw`https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/commands#Key_combinations`; + + ok( + normalized.error.includes(expectedError), + `The manifest error ${JSON.stringify( + normalized.error + )} must contain ${JSON.stringify(expectedError)}` + ); + } + } + + for (let shortcut of validShortcuts) { + validateShortcut(shortcut, true); + } + for (let shortcut of invalidShortcuts) { + validateShortcut(shortcut, false); + } +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js b/browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js new file mode 100644 index 0000000000..f81e7d3cb5 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js @@ -0,0 +1,62 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +async function testKeyword(params) { + let normalized = await ExtensionTestUtils.normalizeManifest({ + omnibox: { + keyword: params.keyword, + }, + }); + + if (params.expectError) { + let expectedError = + String.raw`omnibox.keyword: String "${params.keyword}" ` + + String.raw`must match /^[^?\s:][^\s:]*$/`; + ok( + normalized.error.includes(expectedError), + `The manifest error ${JSON.stringify(normalized.error)} ` + + `must contain ${JSON.stringify(expectedError)}` + ); + } else { + equal(normalized.error, undefined, "Should not have an error"); + equal(normalized.errors.length, 0, "Should not have warnings"); + } +} + +add_task(async function test_manifest_commands() { + // accepted single character keywords + await testKeyword({ keyword: "a", expectError: false }); + await testKeyword({ keyword: "-", expectError: false }); + await testKeyword({ keyword: "嗨", expectError: false }); + await testKeyword({ keyword: "*", expectError: false }); + await testKeyword({ keyword: "/", expectError: false }); + + // rejected single character keywords + await testKeyword({ keyword: "?", expectError: true }); + await testKeyword({ keyword: " ", expectError: true }); + await testKeyword({ keyword: ":", expectError: true }); + + // accepted multi-character keywords + await testKeyword({ keyword: "aa", expectError: false }); + await testKeyword({ keyword: "http", expectError: false }); + await testKeyword({ keyword: "f?a", expectError: false }); + await testKeyword({ keyword: "fa?", expectError: false }); + await testKeyword({ keyword: "f/x", expectError: false }); + await testKeyword({ keyword: "/fx", expectError: false }); + await testKeyword({ keyword: "fx/", expectError: false }); + + // rejected multi-character keywords + await testKeyword({ keyword: " a", expectError: true }); + await testKeyword({ keyword: "a ", expectError: true }); + await testKeyword({ keyword: " ", expectError: true }); + await testKeyword({ keyword: " a ", expectError: true }); + await testKeyword({ keyword: "?fx", expectError: true }); + await testKeyword({ keyword: "f:x", expectError: true }); + await testKeyword({ keyword: "fx:", expectError: true }); + await testKeyword({ keyword: "f x", expectError: true }); + + // miscellaneous tests + await testKeyword({ keyword: "こんにちは", expectError: false }); + await testKeyword({ keyword: "http://", expectError: true }); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js b/browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js new file mode 100644 index 0000000000..fed7af5d5b --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js @@ -0,0 +1,85 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* globals chrome */ + +Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); + +async function testPermission(options) { + function background(bgOptions) { + browser.test.sendMessage("typeof-namespace", { + browser: typeof browser[bgOptions.namespace], + chrome: typeof chrome[bgOptions.namespace], + }); + } + + let extensionDetails = { + background: `(${background})(${JSON.stringify(options)})`, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionDetails); + + await extension.startup(); + + let types = await extension.awaitMessage("typeof-namespace"); + equal( + types.browser, + "undefined", + `Type of browser.${options.namespace} without manifest entry` + ); + equal( + types.chrome, + "undefined", + `Type of chrome.${options.namespace} without manifest entry` + ); + + await extension.unload(); + + extensionDetails.manifest = options.manifest; + extension = ExtensionTestUtils.loadExtension(extensionDetails); + + await extension.startup(); + + types = await extension.awaitMessage("typeof-namespace"); + equal( + types.browser, + "object", + `Type of browser.${options.namespace} with manifest entry` + ); + equal( + types.chrome, + "object", + `Type of chrome.${options.namespace} with manifest entry` + ); + + await extension.unload(); +} + +add_task(async function test_action() { + await testPermission({ + namespace: "action", + manifest: { + manifest_version: 3, + action: {}, + }, + }); +}); + +add_task(async function test_browserAction() { + await testPermission({ + namespace: "browserAction", + manifest: { + browser_action: {}, + }, + }); +}); + +add_task(async function test_pageAction() { + await testPermission({ + namespace: "pageAction", + manifest: { + page_action: {}, + }, + }); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_menu_caller.js b/browser/components/extensions/test/xpcshell/test_ext_menu_caller.js new file mode 100644 index 0000000000..5aa04bbc78 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_menu_caller.js @@ -0,0 +1,53 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(async function test_create_menu_ext_error() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["menus"], + }, + async background() { + let { fileName } = new Error(); + browser.menus.create({ + id: "muted-tab", + title: "open link with Menu 1", + contexts: ["link"], + }); + await new Promise(resolve => { + browser.menus.create( + { + id: "muted-tab", + title: "open link with Menu 2", + contexts: ["link"], + }, + resolve + ); + }); + browser.test.sendMessage("fileName", fileName); + }, + }); + + let fileName; + const { messages } = await promiseConsoleOutput(async () => { + await extension.startup(); + fileName = await extension.awaitMessage("fileName"); + await extension.unload(); + }); + let [msg] = messages + .filter(m => m.message.includes("Unchecked lastError")) + .map(m => m.QueryInterface(Ci.nsIScriptError)); + equal(msg.sourceName, fileName, "Message source"); + + equal( + msg.errorMessage, + "Unchecked lastError value: Error: ID already exists: muted-tab", + "Message content" + ); + equal(msg.lineNumber, 9, "Message line"); + + let frame = msg.stack; + equal(frame.source, fileName, "Frame source"); + equal(frame.line, 9, "Frame line"); + equal(frame.column, 23, "Frame column"); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_menu_startup.js b/browser/components/extensions/test/xpcshell/test_ext_menu_startup.js new file mode 100644 index 0000000000..aa019c6584 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_menu_startup.js @@ -0,0 +1,432 @@ +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs", + Management: "resource://gre/modules/Extension.sys.mjs", +}); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +Services.prefs.setBoolPref("extensions.eventPages.enabled", true); + +function getExtension(id, background, useAddonManager) { + return ExtensionTestUtils.loadExtension({ + useAddonManager, + manifest: { + browser_specific_settings: { gecko: { id } }, + permissions: ["menus"], + background: { persistent: false }, + }, + background, + }); +} + +async function expectCached(extension, expect) { + let { StartupCache } = ExtensionParent; + let cached = await StartupCache.menus.get(extension.id); + let createProperties = Array.from(cached.values()); + equal(cached.size, expect.length, "menus saved in cache"); + // The menus startupCache is a map and the order is significant + // for recreating menus on startup. Ensure that they are in + // the expected order. We only verify specific keys here rather + // than all menu properties. + for (let i in createProperties) { + Assert.deepEqual( + createProperties[i], + expect[i], + "expected cached properties exist" + ); + } +} + +function promiseExtensionEvent(wrapper, event) { + return new Promise(resolve => { + wrapper.extension.once(event, (kind, data) => { + resolve(data); + }); + }); +} + +add_setup(async () => { + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_menu_onInstalled() { + async function background() { + browser.runtime.onInstalled.addListener(async () => { + const parentId = browser.menus.create({ + contexts: ["all"], + title: "parent", + id: "test-parent", + }); + browser.menus.create({ + parentId, + title: "click A", + id: "test-click-a", + }); + browser.menus.create( + { + parentId, + title: "click B", + id: "test-click-b", + }, + () => { + browser.test.sendMessage("onInstalled"); + } + ); + }); + browser.menus.create( + { + contexts: ["tab"], + title: "top-level", + id: "test-top-level", + }, + () => { + browser.test.sendMessage("create", browser.runtime.lastError?.message); + } + ); + + browser.test.onMessage.addListener(async msg => { + browser.test.log(`onMessage ${msg}`); + if (msg == "updatemenu") { + await browser.menus.update("test-click-a", { title: "click updated" }); + } else if (msg == "removemenu") { + await browser.menus.remove("test-click-b"); + } else if (msg == "removeall") { + await browser.menus.removeAll(); + } + browser.test.sendMessage("updated"); + }); + } + + const extension = getExtension( + "test-persist@mochitest", + background, + "permanent" + ); + + await extension.startup(); + let lastError = await extension.awaitMessage("create"); + Assert.equal(lastError, undefined, "no error creating menu"); + await extension.awaitMessage("onInstalled"); + await extension.terminateBackground(); + + await expectCached(extension, [ + { + contexts: ["tab"], + id: "test-top-level", + title: "top-level", + }, + { contexts: ["all"], id: "test-parent", title: "parent" }, + { + id: "test-click-a", + parentId: "test-parent", + title: "click A", + }, + { + id: "test-click-b", + parentId: "test-parent", + title: "click B", + }, + ]); + + await extension.wakeupBackground(); + lastError = await extension.awaitMessage("create"); + Assert.equal( + lastError, + "The menu id test-top-level already exists in menus.create.", + "correct error creating menu" + ); + + await AddonTestUtils.promiseRestartManager(); + await extension.awaitStartup(); + + // verify the startupcache + await expectCached(extension, [ + { + contexts: ["tab"], + id: "test-top-level", + title: "top-level", + }, + { contexts: ["all"], id: "test-parent", title: "parent" }, + { + id: "test-click-a", + parentId: "test-parent", + title: "click A", + }, + { + id: "test-click-b", + parentId: "test-parent", + title: "click B", + }, + ]); + + equal( + extension.extension.backgroundState, + "stopped", + "background is not running" + ); + await extension.wakeupBackground(); + lastError = await extension.awaitMessage("create"); + Assert.equal( + lastError, + "The menu id test-top-level already exists in menus.create.", + "correct error creating menu" + ); + + extension.sendMessage("updatemenu"); + await extension.awaitMessage("updated"); + await extension.terminateBackground(); + + // Title change is cached + await expectCached(extension, [ + { + contexts: ["tab"], + id: "test-top-level", + title: "top-level", + }, + { contexts: ["all"], id: "test-parent", title: "parent" }, + { + id: "test-click-a", + parentId: "test-parent", + title: "click updated", + }, + { + id: "test-click-b", + parentId: "test-parent", + title: "click B", + }, + ]); + + await extension.wakeupBackground(); + lastError = await extension.awaitMessage("create"); + Assert.equal( + lastError, + "The menu id test-top-level already exists in menus.create.", + "correct error creating menu" + ); + + extension.sendMessage("removemenu"); + await extension.awaitMessage("updated"); + await extension.terminateBackground(); + + // menu removed + await expectCached(extension, [ + { + contexts: ["tab"], + id: "test-top-level", + title: "top-level", + }, + { contexts: ["all"], id: "test-parent", title: "parent" }, + { + id: "test-click-a", + parentId: "test-parent", + title: "click updated", + }, + ]); + + await extension.wakeupBackground(); + lastError = await extension.awaitMessage("create"); + Assert.equal( + lastError, + "The menu id test-top-level already exists in menus.create.", + "correct error creating menu" + ); + + extension.sendMessage("removeall"); + await extension.awaitMessage("updated"); + await extension.terminateBackground(); + + // menus removed + await expectCached(extension, []); + + await extension.unload(); +}); + +add_task(async function test_menu_nested() { + async function background() { + browser.test.onMessage.addListener(async (action, properties) => { + browser.test.log(`onMessage ${action}`); + switch (action) { + case "create": + await new Promise(resolve => { + browser.menus.create(properties, resolve); + }); + break; + case "update": + { + let { id, ...update } = properties; + await browser.menus.update(id, update); + } + break; + case "remove": + { + let { id } = properties; + await browser.menus.remove(id); + } + break; + case "removeAll": + await browser.menus.removeAll(); + break; + } + browser.test.sendMessage("updated"); + }); + } + + const extension = getExtension( + "test-nesting@mochitest", + background, + "permanent" + ); + await extension.startup(); + + extension.sendMessage("create", { + id: "first", + contexts: ["all"], + title: "first", + }); + await extension.awaitMessage("updated"); + await expectCached(extension, [ + { contexts: ["all"], id: "first", title: "first" }, + ]); + + extension.sendMessage("create", { + id: "second", + contexts: ["all"], + title: "second", + }); + await extension.awaitMessage("updated"); + await expectCached(extension, [ + { contexts: ["all"], id: "first", title: "first" }, + { contexts: ["all"], id: "second", title: "second" }, + ]); + + extension.sendMessage("create", { + id: "third", + contexts: ["all"], + title: "third", + parentId: "first", + }); + await extension.awaitMessage("updated"); + await expectCached(extension, [ + { contexts: ["all"], id: "first", title: "first" }, + { contexts: ["all"], id: "second", title: "second" }, + { + contexts: ["all"], + id: "third", + parentId: "first", + title: "third", + }, + ]); + + extension.sendMessage("create", { + id: "fourth", + contexts: ["all"], + title: "fourth", + }); + await extension.awaitMessage("updated"); + await expectCached(extension, [ + { contexts: ["all"], id: "first", title: "first" }, + { contexts: ["all"], id: "second", title: "second" }, + { + contexts: ["all"], + id: "third", + parentId: "first", + title: "third", + }, + { contexts: ["all"], id: "fourth", title: "fourth" }, + ]); + + extension.sendMessage("update", { + id: "first", + parentId: "second", + }); + await extension.awaitMessage("updated"); + await expectCached(extension, [ + { contexts: ["all"], id: "second", title: "second" }, + { contexts: ["all"], id: "fourth", title: "fourth" }, + { + contexts: ["all"], + id: "first", + title: "first", + parentId: "second", + }, + { + contexts: ["all"], + id: "third", + parentId: "first", + title: "third", + }, + ]); + + await AddonTestUtils.promiseShutdownManager(); + // We need to attach an event listener before the + // startup event is emitted. Fortunately, we + // emit via Management before emitting on extension. + let promiseMenus; + Management.once("startup", (kind, ext) => { + info(`management ${kind} ${ext.id}`); + promiseMenus = promiseExtensionEvent( + { extension: ext }, + "webext-menus-created" + ); + }); + await AddonTestUtils.promiseStartupManager(); + await extension.awaitStartup(); + await extension.wakeupBackground(); + + await expectCached(extension, [ + { contexts: ["all"], id: "second", title: "second" }, + { contexts: ["all"], id: "fourth", title: "fourth" }, + { + contexts: ["all"], + id: "first", + title: "first", + parentId: "second", + }, + { + contexts: ["all"], + id: "third", + parentId: "first", + title: "third", + }, + ]); + // validate nesting + let menus = await promiseMenus; + equal(menus.get("first").parentId, "second", "menuitem parent is correct"); + equal( + menus.get("second").children.length, + 1, + "menuitem parent has correct number of children" + ); + equal( + menus.get("second").root.children.length, + 2, // second and forth + "menuitem root has correct number of children" + ); + + extension.sendMessage("remove", { + id: "second", + }); + await extension.awaitMessage("updated"); + await expectCached(extension, [ + { contexts: ["all"], id: "fourth", title: "fourth" }, + ]); + + extension.sendMessage("removeAll"); + await extension.awaitMessage("updated"); + await expectCached(extension, []); + + await extension.unload(); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_normandyAddonStudy.js b/browser/components/extensions/test/xpcshell/test_ext_normandyAddonStudy.js new file mode 100644 index 0000000000..0a2a9dcd49 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_normandyAddonStudy.js @@ -0,0 +1,243 @@ +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", +}); + +const { AddonStudies } = ChromeUtils.importESModule( + "resource://normandy/lib/AddonStudies.sys.mjs" +); +const { NormandyTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/NormandyTestUtils.sys.mjs" +); +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); +var { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { addonStudyFactory } = NormandyTestUtils.factories; + +AddonTestUtils.init(this); + +// All tests run privileged unless otherwise specified not to. +function createExtension(backgroundScript, permissions, isPrivileged = true) { + let extensionData = { + background: backgroundScript, + manifest: { + browser_specific_settings: { + gecko: { + id: "test@shield.mozilla.com", + }, + }, + permissions, + }, + isPrivileged, + }; + return ExtensionTestUtils.loadExtension(extensionData); +} + +async function run(test) { + let extension = createExtension( + test.backgroundScript, + test.permissions || ["normandyAddonStudy"], + test.isPrivileged + ); + const promiseValidation = test.validationScript + ? test.validationScript(extension) + : Promise.resolve(); + + await extension.startup(); + + await promiseValidation; + + if (test.doneSignal) { + await extension.awaitFinish(test.doneSignal); + } + + await extension.unload(); +} + +add_task(async function setup() { + await ExtensionTestUtils.startAddonManager(); +}); + +add_task( + async function test_normandyAddonStudy_without_normandyAddonStudy_permission_privileged() { + await run({ + backgroundScript: () => { + browser.test.assertTrue( + !browser.normandyAddonStudy, + "'normandyAddonStudy' permission is required" + ); + browser.test.notifyPass("normandyAddonStudy_permission"); + }, + permissions: [], + doneSignal: "normandyAddonStudy_permission", + }); + } +); + +add_task(async function test_normandyAddonStudy_without_privilege() { + await run({ + backgroundScript: () => { + browser.test.assertTrue( + !browser.normandyAddonStudy, + "Extension must be privileged" + ); + browser.test.notifyPass("normandyAddonStudy_permission"); + }, + isPrivileged: false, + doneSignal: "normandyAddonStudy_permission", + }); +}); + +add_task(async function test_normandyAddonStudy_temporary_without_privilege() { + let extension = ExtensionTestUtils.loadExtension({ + temporarilyInstalled: true, + isPrivileged: false, + manifest: { + permissions: ["normandyAddonStudy"], + }, + }); + ExtensionTestUtils.failOnSchemaWarnings(false); + let { messages } = await promiseConsoleOutput(async () => { + await Assert.rejects( + extension.startup(), + /Using the privileged permission/, + "Startup failed with privileged permission" + ); + }); + ExtensionTestUtils.failOnSchemaWarnings(true); + AddonTestUtils.checkMessages( + messages, + { + expected: [ + { + message: + /Using the privileged permission 'normandyAddonStudy' requires a privileged add-on/, + }, + ], + }, + true + ); +}); + +add_task(async function test_getStudy_works() { + const study = addonStudyFactory({ + addonId: "test@shield.mozilla.com", + }); + + const testWrapper = AddonStudies.withStudies([study]); + const test = testWrapper(async () => { + await run({ + backgroundScript: async () => { + const result = await browser.normandyAddonStudy.getStudy(); + browser.test.sendMessage("study", result); + }, + validationScript: async extension => { + let studyResult = await extension.awaitMessage("study"); + deepEqual( + studyResult, + study, + "normandyAddonStudy.getStudy returns the correct study" + ); + }, + }); + }); + + await test(); +}); + +add_task(async function test_endStudy_works() { + const study = addonStudyFactory({ + addonId: "test@shield.mozilla.com", + }); + + const testWrapper = AddonStudies.withStudies([study]); + const test = testWrapper(async () => { + await run({ + backgroundScript: async () => { + await browser.normandyAddonStudy.endStudy("test"); + }, + validationScript: async () => { + // Check that `AddonStudies.markAsEnded` was called + await TestUtils.topicObserved( + "shield-study-ended", + (subject, message) => { + return message === `${study.recipeId}`; + } + ); + + const addon = await AddonManager.getAddonByID(study.addonId); + equal(addon, undefined, "Addon should be uninstalled."); + }, + }); + }); + + await test(); +}); + +add_task(async function test_getClientMetadata_works() { + const study = addonStudyFactory({ + addonId: "test@shield.mozilla.com", + slug: "test-slug", + branch: "test-branch", + }); + + const testWrapper = AddonStudies.withStudies([study]); + const test = testWrapper(async () => { + await run({ + backgroundScript: async () => { + const metadata = await browser.normandyAddonStudy.getClientMetadata(); + browser.test.sendMessage("clientMetadata", metadata); + }, + validationScript: async extension => { + let clientMetadata = await extension.awaitMessage("clientMetadata"); + + Assert.strictEqual( + clientMetadata.updateChannel, + Services.appinfo.defaultUpdateChannel, + "clientMetadata contains correct updateChannel" + ); + + Assert.strictEqual( + clientMetadata.fxVersion, + Services.appinfo.version, + "clientMetadata contains correct fxVersion" + ); + + ok("clientID" in clientMetadata, "clientMetadata contains a clientID"); + }, + }); + }); + + await test(); +}); + +add_task(async function test_onUnenroll_works() { + const study = addonStudyFactory({ + addonId: "test@shield.mozilla.com", + }); + + const testWrapper = AddonStudies.withStudies([study]); + const test = testWrapper(async () => { + await run({ + backgroundScript: () => { + browser.normandyAddonStudy.onUnenroll.addListener(reason => { + browser.test.sendMessage("unenrollReason", reason); + }); + browser.test.sendMessage("bgpageReady"); + }, + validationScript: async extension => { + await extension.awaitMessage("bgpageReady"); + await AddonStudies.markAsEnded(study, "test"); + const unenrollReason = await extension.awaitMessage("unenrollReason"); + equal(unenrollReason, "test", "Unenroll listener should be called."); + }, + }); + }); + + await test(); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js b/browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js new file mode 100644 index 0000000000..bd462ec9b6 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js @@ -0,0 +1,81 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +// Load lazy so we create the app info first. +ChromeUtils.defineESModuleGetters(this, { + PageActions: "resource:///modules/PageActions.sys.mjs", +}); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { createAppInfo, promiseShutdownManager, promiseStartupManager } = + AddonTestUtils; + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "58"); + +// This is copied and pasted from ext-browser.js and used in ext-pageAction.js. +// It's used as the PageActions action ID. +function makeWidgetId(id) { + id = id.toLowerCase(); + // FIXME: This allows for collisions. + return id.replace(/[^a-z0-9_-]/g, "_"); +} + +// Tests that the pinnedToUrlbar property of the PageActions.Action object +// backing the extension's page action persists across app restarts. +add_task(async function testAppShutdown() { + let extensionData = { + useAddonManager: "permanent", + manifest: { + page_action: { + default_title: "test_ext_pageAction_shutdown.js", + browser_style: false, + }, + }, + }; + + // Simulate starting up the app. + PageActions.init(); + await promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + // Get the PageAction.Action object. Its pinnedToUrlbar should have been + // initialized to true in ext-pageAction.js, when it's created. + let actionID = makeWidgetId(extension.id); + let action = PageActions.actionForID(actionID); + Assert.equal(action.pinnedToUrlbar, true); + + // Simulate restarting the app without first unloading the extension. + await promiseShutdownManager(); + PageActions._reset(); + await promiseStartupManager(); + await extension.awaitStartup(); + + // Get the action. Its pinnedToUrlbar should remain true. + action = PageActions.actionForID(actionID); + Assert.equal(action.pinnedToUrlbar, true); + + // Now set its pinnedToUrlbar to false. + action.pinnedToUrlbar = false; + + // Simulate restarting the app again without first unloading the extension. + await promiseShutdownManager(); + PageActions._reset(); + await promiseStartupManager(); + await extension.awaitStartup(); + + action = PageActions.actionForID(actionID); + Assert.equal(action.pinnedToUrlbar, true); + + // Now unload the extension and quit the app. + await extension.unload(); + await promiseShutdownManager(); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js b/browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js new file mode 100644 index 0000000000..8c713191cc --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js @@ -0,0 +1,300 @@ +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + MockRegistry: "resource://testing-common/MockRegistry.sys.mjs", + ctypes: "resource://gre/modules/ctypes.sys.mjs", +}); + +do_get_profile(); + +let tmpDir; +let baseDir; +let slug = + AppConstants.platform === "linux" ? "pkcs11-modules" : "PKCS11Modules"; + +add_task(async function setupTest() { + tmpDir = await IOUtils.createUniqueDirectory( + Services.dirsvc.get("TmpD", Ci.nsIFile).path, + "PKCS11" + ); + + baseDir = PathUtils.join(tmpDir, slug); + await IOUtils.makeDirectory(baseDir); +}); + +registerCleanupFunction(async () => { + await IOUtils.remove(tmpDir, { recursive: true }); +}); + +const testmodule = PathUtils.join( + PathUtils.parent(Services.dirsvc.get("CurWorkD", Ci.nsIFile).path, 5), + "security", + "manager", + "ssl", + "tests", + "unit", + "pkcs11testmodule", + ctypes.libraryName("pkcs11testmodule") +); + +// This function was inspired by the native messaging test under +// toolkit/components/extensions + +async function setupManifests(modules) { + async function writeManifest(module) { + let manifest = { + name: module.name, + description: module.description, + path: module.path, + type: "pkcs11", + allowed_extensions: [module.id], + }; + + let manifestPath = PathUtils.join(baseDir, `${module.name}.json`); + await IOUtils.writeJSON(manifestPath, manifest); + + return manifestPath; + } + + switch (AppConstants.platform) { + case "macosx": + case "linux": + let dirProvider = { + getFile(property) { + if ( + property == "XREUserNativeManifests" || + property == "XRESysNativeManifests" + ) { + return new FileUtils.File(tmpDir); + } + return null; + }, + }; + + Services.dirsvc.registerProvider(dirProvider); + registerCleanupFunction(() => { + Services.dirsvc.unregisterProvider(dirProvider); + }); + + for (let module of modules) { + await writeManifest(module); + } + break; + + case "win": + const REGKEY = String.raw`Software\Mozilla\PKCS11Modules`; + + let registry = new MockRegistry(); + registerCleanupFunction(() => { + registry.shutdown(); + }); + + for (let module of modules) { + let manifestPath = await writeManifest(module); + registry.setValue( + Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + `${REGKEY}\\${module.name}`, + "", + manifestPath + ); + } + break; + + default: + ok( + false, + `Loading of PKCS#11 modules is not supported on ${AppConstants.platform}` + ); + } +} + +add_task(async function test_pkcs11() { + async function background() { + try { + const { os } = await browser.runtime.getPlatformInfo(); + if (os !== "win") { + // Expect this call to not throw (explicitly cover regression fixed in Bug 1759162). + let isInstalledNonAbsolute = await browser.pkcs11.isModuleInstalled( + "testmoduleNonAbsolutePath" + ); + browser.test.assertFalse( + isInstalledNonAbsolute, + "PKCS#11 module with non absolute path expected to not be installed" + ); + } + let isInstalled = await browser.pkcs11.isModuleInstalled("testmodule"); + browser.test.assertFalse( + isInstalled, + "PKCS#11 module is not installed before we install it" + ); + await browser.pkcs11.installModule("testmodule", 0); + isInstalled = await browser.pkcs11.isModuleInstalled("testmodule"); + browser.test.assertTrue( + isInstalled, + "PKCS#11 module is installed after we install it" + ); + let slots = await browser.pkcs11.getModuleSlots("testmodule"); + browser.test.assertEq( + "Test PKCS11 Slot", + slots[0].name, + "The first slot name matches the expected name" + ); + browser.test.assertEq( + "Test PKCS11 Slot 二", + slots[1].name, + "The second slot name matches the expected name" + ); + browser.test.assertTrue(slots[1].token, "The second slot has a token"); + browser.test.assertFalse(slots[2].token, "The third slot has no token"); + browser.test.assertEq( + "Test PKCS11 Tokeñ 2 Label", + slots[1].token.name, + "The token name matches the expected name" + ); + browser.test.assertEq( + "Test PKCS11 Manufacturer ID", + slots[1].token.manufacturer, + "The token manufacturer matches the expected manufacturer" + ); + browser.test.assertEq( + "0.0", + slots[1].token.HWVersion, + "The token hardware version matches the expected version" + ); + browser.test.assertEq( + "0.0", + slots[1].token.FWVersion, + "The token firmware version matches the expected version" + ); + browser.test.assertEq( + "", + slots[1].token.serial, + "The token has no serial number" + ); + browser.test.assertFalse( + slots[1].token.isLoggedIn, + "The token is not logged in" + ); + await browser.pkcs11.uninstallModule("testmodule"); + isInstalled = await browser.pkcs11.isModuleInstalled("testmodule"); + browser.test.assertFalse( + isInstalled, + "PKCS#11 module is no longer installed after we uninstall it" + ); + await browser.pkcs11.installModule("testmodule"); + isInstalled = await browser.pkcs11.isModuleInstalled("testmodule"); + browser.test.assertTrue( + isInstalled, + "Installing the PKCS#11 module without flags parameter succeeds" + ); + await browser.pkcs11.uninstallModule("testmodule"); + await browser.test.assertRejects( + browser.pkcs11.isModuleInstalled("nonexistingmodule"), + /No such PKCS#11 module nonexistingmodule/, + "We cannot access modules if no JSON file exists" + ); + await browser.test.assertRejects( + browser.pkcs11.isModuleInstalled("othermodule"), + /No such PKCS#11 module othermodule/, + "We cannot access modules if we're not listed in the module's manifest file's allowed_extensions key" + ); + await browser.test.assertRejects( + browser.pkcs11.uninstallModule("internalmodule"), + /No such PKCS#11 module internalmodule/, + "We cannot uninstall the NSS Builtin Roots Module" + ); + await browser.test.assertRejects( + browser.pkcs11.installModule("osclientcerts", 0), + /No such PKCS#11 module osclientcerts/, + "installModule should not work on the built-in osclientcerts module" + ); + await browser.test.assertRejects( + browser.pkcs11.uninstallModule("osclientcerts"), + /No such PKCS#11 module osclientcerts/, + "uninstallModule should not work on the built-in osclientcerts module" + ); + await browser.test.assertRejects( + browser.pkcs11.isModuleInstalled("osclientcerts"), + /No such PKCS#11 module osclientcerts/, + "isModuleLoaded should not work on the built-in osclientcerts module" + ); + await browser.test.assertRejects( + browser.pkcs11.getModuleSlots("osclientcerts"), + /No such PKCS#11 module osclientcerts/, + "getModuleSlots should not work on the built-in osclientcerts module" + ); + await browser.test.assertRejects( + browser.pkcs11.installModule("ipcclientcerts", 0), + /No such PKCS#11 module ipcclientcerts/, + "installModule should not work on the built-in ipcclientcerts module" + ); + await browser.test.assertRejects( + browser.pkcs11.uninstallModule("ipcclientcerts"), + /No such PKCS#11 module ipcclientcerts/, + "uninstallModule should not work on the built-in ipcclientcerts module" + ); + await browser.test.assertRejects( + browser.pkcs11.isModuleInstalled("ipcclientcerts"), + /No such PKCS#11 module ipcclientcerts/, + "isModuleLoaded should not work on the built-in ipcclientcerts module" + ); + await browser.test.assertRejects( + browser.pkcs11.getModuleSlots("ipcclientcerts"), + /No such PKCS#11 module ipcclientcerts/, + "getModuleSlots should not work on the built-in ipcclientcerts module" + ); + browser.test.notifyPass("pkcs11"); + } catch (e) { + browser.test.fail(`Error: ${String(e)} :: ${e.stack}`); + browser.test.notifyFail("pkcs11 failed"); + } + } + + let libDir = FileUtils.getDir("GreBinD", []); + await setupManifests([ + { + name: "testmodule", + description: "PKCS#11 Test Module", + path: testmodule, + id: "pkcs11@tests.mozilla.org", + }, + { + name: "testmoduleNonAbsolutePath", + description: "PKCS#11 Test Module", + path: ctypes.libraryName("pkcs11testmodule"), + id: "pkcs11@tests.mozilla.org", + }, + { + name: "othermodule", + description: "PKCS#11 Test Module", + path: testmodule, + id: "other@tests.mozilla.org", + }, + { + name: "internalmodule", + description: "Builtin Roots Module", + path: PathUtils.join( + Services.dirsvc.get("CurWorkD", Ci.nsIFile).path, + ctypes.libraryName("nssckbi") + ), + id: "pkcs11@tests.mozilla.org", + }, + { + name: "osclientcerts", + description: "OS Client Cert Module", + path: PathUtils.join(libDir.path, ctypes.libraryName("osclientcerts")), + id: "pkcs11@tests.mozilla.org", + }, + ]); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["pkcs11"], + browser_specific_settings: { gecko: { id: "pkcs11@tests.mozilla.org" } }, + }, + background: background, + }); + await extension.startup(); + await extension.awaitFinish("pkcs11"); + await extension.unload(); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_defaults.js b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_defaults.js new file mode 100644 index 0000000000..dd24be3aff --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_defaults.js @@ -0,0 +1,263 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); + +const { SearchUtils } = ChromeUtils.importESModule( + "resource://gre/modules/SearchUtils.sys.mjs" +); + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +const URLTYPE_SUGGEST_JSON = "application/x-suggestions+json"; + +AddonTestUtils.init(this); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +const kSearchEngineURL = "https://example.com/?q={searchTerms}&foo=myparams"; +const kSuggestURL = "https://example.com/fake/suggest/"; +const kSuggestURLParams = "q={searchTerms}&type=list2"; + +Services.prefs.setBoolPref("browser.search.log", true); + +add_task(async function setup() { + AddonTestUtils.usePrivilegedSignatures = false; + AddonTestUtils.overrideCertDB(); + await AddonTestUtils.promiseStartupManager(); + await SearchTestUtils.useTestEngines("data", null, [ + { + webExtension: { + id: "test@search.mozilla.org", + }, + appliesTo: [ + { + included: { everywhere: true }, + default: "yes", + }, + ], + }, + { + webExtension: { + id: "test2@search.mozilla.org", + }, + appliesTo: [ + { + included: { everywhere: true }, + }, + ], + }, + ]); + await Services.search.init(); + registerCleanupFunction(async () => { + await AddonTestUtils.promiseShutdownManager(); + }); +}); + +function assertEngineParameters({ + name, + searchURL, + suggestionURL, + messageSnippet, +}) { + let engine = Services.search.getEngineByName(name); + Assert.ok(engine, `Should have found ${name}`); + + Assert.equal( + engine.getSubmission("{searchTerms}").uri.spec, + encodeURI(searchURL), + `Should have ${messageSnippet} the suggestion url.` + ); + Assert.equal( + engine.getSubmission("{searchTerms}", URLTYPE_SUGGEST_JSON)?.uri.spec, + suggestionURL ? encodeURI(suggestionURL) : suggestionURL, + `Should ${messageSnippet} the submission URL.` + ); +} + +add_task(async function test_extension_changing_to_app_provided_default() { + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + icons: { + 16: "foo.ico", + }, + chrome_settings_overrides: { + search_provider: { + is_default: true, + name: "MozParamsTest2", + keyword: "MozSearch", + search_url: kSearchEngineURL, + suggest_url: kSuggestURL, + suggest_url_get_params: kSuggestURLParams, + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + await AddonTestUtils.waitForSearchProviderStartup(ext1); + + Assert.equal( + Services.search.defaultEngine.name, + "MozParamsTest2", + "Should have switched the default engine." + ); + + assertEngineParameters({ + name: "MozParamsTest2", + searchURL: "https://example.com/2/?q={searchTerms}&simple2=5", + messageSnippet: "left unchanged", + }); + + let promiseDefaultBrowserChange = SearchTestUtils.promiseSearchNotification( + "engine-default", + "browser-search-engine-modified" + ); + await ext1.unload(); + await promiseDefaultBrowserChange; + + Assert.equal( + Services.search.defaultEngine.name, + "MozParamsTest", + "Should have reverted to the original default engine." + ); +}); + +add_task(async function test_extension_overriding_app_provided_default() { + const settings = await RemoteSettings(SearchUtils.SETTINGS_ALLOWLIST_KEY); + sinon.stub(settings, "get").returns([ + { + thirdPartyId: "test@thirdparty.example.com", + overridesId: "test2@search.mozilla.org", + urls: [ + { + search_url: "https://example.com/?q={searchTerms}&foo=myparams", + }, + ], + }, + ]); + + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { + id: "test@thirdparty.example.com", + }, + }, + icons: { + 16: "foo.ico", + }, + chrome_settings_overrides: { + search_provider: { + is_default: true, + name: "MozParamsTest2", + keyword: "MozSearch", + search_url: kSearchEngineURL, + suggest_url: kSuggestURL, + suggest_url_get_params: kSuggestURLParams, + }, + }, + }, + useAddonManager: "permanent", + }); + + info("startup"); + + await ext1.startup(); + await AddonTestUtils.waitForSearchProviderStartup(ext1); + + Assert.equal( + Services.search.defaultEngine.name, + "MozParamsTest2", + "Should have switched the default engine." + ); + assertEngineParameters({ + name: "MozParamsTest2", + searchURL: kSearchEngineURL, + suggestionURL: `${kSuggestURL}?${kSuggestURLParams}`, + messageSnippet: "changed", + }); + + info("disable"); + + let promiseDefaultBrowserChange = SearchTestUtils.promiseSearchNotification( + "engine-default", + "browser-search-engine-modified" + ); + await ext1.addon.disable(); + await promiseDefaultBrowserChange; + + Assert.equal( + Services.search.defaultEngine.name, + "MozParamsTest", + "Should have reverted to the original default engine." + ); + assertEngineParameters({ + name: "MozParamsTest2", + searchURL: "https://example.com/2/?q={searchTerms}&simple2=5", + messageSnippet: "reverted", + }); + + info("enable"); + + promiseDefaultBrowserChange = SearchTestUtils.promiseSearchNotification( + "engine-default", + "browser-search-engine-modified" + ); + await ext1.addon.enable(); + await promiseDefaultBrowserChange; + + Assert.equal( + Services.search.defaultEngine.name, + "MozParamsTest2", + "Should have switched the default engine." + ); + + assertEngineParameters({ + name: "MozParamsTest2", + searchURL: kSearchEngineURL, + suggestionURL: `${kSuggestURL}?${kSuggestURLParams}`, + messageSnippet: "changed", + }); + + info("unload"); + + promiseDefaultBrowserChange = SearchTestUtils.promiseSearchNotification( + "engine-default", + "browser-search-engine-modified" + ); + await ext1.unload(); + await promiseDefaultBrowserChange; + + Assert.equal( + Services.search.defaultEngine.name, + "MozParamsTest", + "Should have reverted to the original default engine." + ); + + assertEngineParameters({ + name: "MozParamsTest2", + searchURL: "https://example.com/2/?q={searchTerms}&simple2=5", + messageSnippet: "reverted", + }); + sinon.restore(); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search.js b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search.js new file mode 100644 index 0000000000..10fed4d36b --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search.js @@ -0,0 +1,597 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +let delay = () => new Promise(resolve => setTimeout(resolve, 0)); + +const kSearchFormURL = "https://example.com/searchform"; +const kSearchEngineURL = "https://example.com/?search={searchTerms}"; +const kSearchSuggestURL = "https://example.com/?suggest={searchTerms}"; +const kSearchTerm = "foo"; +const kSearchTermIntl = "日"; +const URLTYPE_SUGGEST_JSON = "application/x-suggestions+json"; + +AddonTestUtils.init(this); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +add_task(async function setup() { + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +}); + +add_task(async function test_extension_adding_engine() { + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + icons: { + 16: "foo.ico", + 32: "foo32.ico", + }, + chrome_settings_overrides: { + search_provider: { + name: "MozSearch", + keyword: "MozSearch", + search_form: kSearchFormURL, + search_url: kSearchEngineURL, + suggest_url: kSearchSuggestURL, + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + await AddonTestUtils.waitForSearchProviderStartup(ext1); + + let engine = Services.search.getEngineByName("MozSearch"); + ok(engine, "Engine should exist."); + + let { baseURI } = ext1.extension; + equal( + engine.getIconURL(), + baseURI.resolve("foo.ico"), + "16x16 icon path matches" + ); + equal( + engine.getIconURL(16), + baseURI.resolve("foo.ico"), + "16x16 icon path matches" + ); + // TODO: Bug 1871036 - Differently sized icons are currently incorrectly + // handled for add-ons. + // equal( + // engine.getIconURL(32), + // baseURI.resolve("foo32.ico"), + // "32x32 icon path matches" + // ); + + let expectedSuggestURL = kSearchSuggestURL.replace( + "{searchTerms}", + kSearchTerm + ); + let submissionSuggest = engine.getSubmission( + kSearchTerm, + URLTYPE_SUGGEST_JSON + ); + let encodedSubmissionURL = engine.getSubmission(kSearchTermIntl).uri.spec; + let testSubmissionURL = kSearchEngineURL.replace( + "{searchTerms}", + encodeURIComponent(kSearchTermIntl) + ); + equal( + encodedSubmissionURL, + testSubmissionURL, + "Encoded UTF-8 URLs should match" + ); + + equal( + submissionSuggest.uri.spec, + expectedSuggestURL, + "Suggest URLs should match" + ); + + equal(engine.searchForm, kSearchFormURL, "Search form URLs should match"); + await ext1.unload(); + await delay(); + + engine = Services.search.getEngineByName("MozSearch"); + ok(!engine, "Engine should not exist"); +}); + +add_task(async function test_extension_adding_engine_with_spaces() { + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + chrome_settings_overrides: { + search_provider: { + name: "MozSearch ", + keyword: "MozSearch", + search_url: "https://example.com/?q={searchTerms}", + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + await AddonTestUtils.waitForSearchProviderStartup(ext1); + + let engine = Services.search.getEngineByName("MozSearch"); + ok(engine, "Engine should exist."); + + await ext1.unload(); + await delay(); + + engine = Services.search.getEngineByName("MozSearch"); + ok(!engine, "Engine should not exist"); +}); + +add_task(async function test_upgrade_default_position_engine() { + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + chrome_settings_overrides: { + search_provider: { + name: "MozSearch", + keyword: "MozSearch", + search_url: "https://example.com/?q={searchTerms}", + }, + }, + browser_specific_settings: { + gecko: { + id: "testengine@mozilla.com", + }, + }, + version: "0.1", + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + await AddonTestUtils.waitForSearchProviderStartup(ext1); + + let engine = Services.search.getEngineByName("MozSearch"); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.moveEngine(engine, 1); + + await ext1.upgrade({ + manifest: { + chrome_settings_overrides: { + search_provider: { + name: "MozSearch", + keyword: "MozSearch", + search_url: "https://example.com/?q={searchTerms}", + }, + }, + browser_specific_settings: { + gecko: { + id: "testengine@mozilla.com", + }, + }, + version: "0.2", + }, + useAddonManager: "temporary", + }); + await AddonTestUtils.waitForSearchProviderStartup(ext1); + + engine = Services.search.getEngineByName("MozSearch"); + equal( + Services.search.defaultEngine, + engine, + "Default engine should still be MozSearch" + ); + equal( + (await Services.search.getEngines()).map(e => e.name).indexOf(engine.name), + 1, + "Engine is in position 1" + ); + + await ext1.unload(); + await delay(); + + engine = Services.search.getEngineByName("MozSearch"); + ok(!engine, "Engine should not exist"); +}); + +add_task(async function test_extension_get_params() { + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + chrome_settings_overrides: { + search_provider: { + name: "MozSearch", + keyword: "MozSearch", + search_url: kSearchEngineURL, + search_url_get_params: "foo=bar&bar=foo", + suggest_url: kSearchSuggestURL, + suggest_url_get_params: "foo=bar&bar=foo", + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + await AddonTestUtils.waitForSearchProviderStartup(ext1); + + let engine = Services.search.getEngineByName("MozSearch"); + ok(engine, "Engine should exist."); + + let url = engine.wrappedJSObject._getURLOfType("text/html"); + equal(url.method, "GET", "Search URLs method is GET"); + + let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm); + let submission = engine.getSubmission(kSearchTerm); + equal( + submission.uri.spec, + `${expectedURL}&foo=bar&bar=foo`, + "Search URLs should match" + ); + + let expectedSuggestURL = kSearchSuggestURL.replace( + "{searchTerms}", + kSearchTerm + ); + let submissionSuggest = engine.getSubmission( + kSearchTerm, + URLTYPE_SUGGEST_JSON + ); + equal( + submissionSuggest.uri.spec, + `${expectedSuggestURL}&foo=bar&bar=foo`, + "Suggest URLs should match" + ); + + await ext1.unload(); +}); + +add_task(async function test_extension_post_params() { + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + chrome_settings_overrides: { + search_provider: { + name: "MozSearch", + keyword: "MozSearch", + search_url: kSearchEngineURL, + search_url_post_params: "foo=bar&bar=foo", + suggest_url: kSearchSuggestURL, + suggest_url_post_params: "foo=bar&bar=foo", + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + await AddonTestUtils.waitForSearchProviderStartup(ext1); + + let engine = Services.search.getEngineByName("MozSearch"); + ok(engine, "Engine should exist."); + + let url = engine.wrappedJSObject._getURLOfType("text/html"); + equal(url.method, "POST", "Search URLs method is POST"); + + let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm); + let submission = engine.getSubmission(kSearchTerm); + equal(submission.uri.spec, expectedURL, "Search URLs should match"); + // postData is a nsIMIMEInputStream which contains a nsIStringInputStream. + equal( + submission.postData.data.data, + "foo=bar&bar=foo", + "Search postData should match" + ); + + let expectedSuggestURL = kSearchSuggestURL.replace( + "{searchTerms}", + kSearchTerm + ); + let submissionSuggest = engine.getSubmission( + kSearchTerm, + URLTYPE_SUGGEST_JSON + ); + equal( + submissionSuggest.uri.spec, + expectedSuggestURL, + "Suggest URLs should match" + ); + equal( + submissionSuggest.postData.data.data, + "foo=bar&bar=foo", + "Suggest postData should match" + ); + + await ext1.unload(); +}); + +add_task(async function test_extension_no_query_params() { + const ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + chrome_settings_overrides: { + search_provider: { + name: "MozSearch", + keyword: "MozSearch", + search_url: "https://example.com/{searchTerms}", + suggest_url: "https://example.com/suggest/{searchTerms}", + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + await AddonTestUtils.waitForSearchProviderStartup(ext1); + + let engine = Services.search.getEngineByName("MozSearch"); + ok(engine, "Engine should exist."); + + const encodedSubmissionURL = engine.getSubmission(kSearchTermIntl).uri.spec; + const testSubmissionURL = + "https://example.com/" + encodeURIComponent(kSearchTermIntl); + equal( + encodedSubmissionURL, + testSubmissionURL, + "Encoded UTF-8 URLs should match" + ); + + const expectedSuggestURL = "https://example.com/suggest/" + kSearchTerm; + let submissionSuggest = engine.getSubmission( + kSearchTerm, + URLTYPE_SUGGEST_JSON + ); + equal( + submissionSuggest.uri.spec, + expectedSuggestURL, + "Suggest URLs should match" + ); + + await ext1.unload(); + await delay(); + + engine = Services.search.getEngineByName("MozSearch"); + ok(!engine, "Engine should not exist"); +}); + +add_task(async function test_extension_empty_suggestUrl() { + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + default_locale: "en", + chrome_settings_overrides: { + search_provider: { + name: "MozSearch", + keyword: "MozSearch", + search_url: kSearchEngineURL, + search_url_post_params: "foo=bar&bar=foo", + suggest_url: "__MSG_suggestUrl__", + suggest_url_get_params: "__MSG_suggestUrlGetParams__", + }, + }, + }, + useAddonManager: "temporary", + files: { + "_locales/en/messages.json": { + suggestUrl: { + message: "", + }, + suggestUrlGetParams: { + message: "", + }, + }, + }, + }); + + await ext1.startup(); + await AddonTestUtils.waitForSearchProviderStartup(ext1); + + let engine = Services.search.getEngineByName("MozSearch"); + ok(engine, "Engine should exist."); + + let url = engine.wrappedJSObject._getURLOfType("text/html"); + equal(url.method, "POST", "Search URLs method is POST"); + + let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm); + let submission = engine.getSubmission(kSearchTerm); + equal(submission.uri.spec, expectedURL, "Search URLs should match"); + // postData is a nsIMIMEInputStream which contains a nsIStringInputStream. + equal( + submission.postData.data.data, + "foo=bar&bar=foo", + "Search postData should match" + ); + + let submissionSuggest = engine.getSubmission( + kSearchTerm, + URLTYPE_SUGGEST_JSON + ); + ok(!submissionSuggest, "There should be no suggest URL."); + + await ext1.unload(); +}); + +add_task(async function test_extension_empty_suggestUrl_with_params() { + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + default_locale: "en", + chrome_settings_overrides: { + search_provider: { + name: "MozSearch", + keyword: "MozSearch", + search_url: kSearchEngineURL, + search_url_post_params: "foo=bar&bar=foo", + suggest_url: "__MSG_suggestUrl__", + suggest_url_get_params: "__MSG_suggestUrlGetParams__", + }, + }, + }, + useAddonManager: "temporary", + files: { + "_locales/en/messages.json": { + suggestUrl: { + message: "", + }, + suggestUrlGetParams: { + message: "abc", + }, + }, + }, + }); + + await ext1.startup(); + await AddonTestUtils.waitForSearchProviderStartup(ext1); + + let engine = Services.search.getEngineByName("MozSearch"); + ok(engine, "Engine should exist."); + + let url = engine.wrappedJSObject._getURLOfType("text/html"); + equal(url.method, "POST", "Search URLs method is POST"); + + let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm); + let submission = engine.getSubmission(kSearchTerm); + equal(submission.uri.spec, expectedURL, "Search URLs should match"); + // postData is a nsIMIMEInputStream which contains a nsIStringInputStream. + equal( + submission.postData.data.data, + "foo=bar&bar=foo", + "Search postData should match" + ); + + let submissionSuggest = engine.getSubmission( + kSearchTerm, + URLTYPE_SUGGEST_JSON + ); + ok(!submissionSuggest, "There should be no suggest URL."); + + await ext1.unload(); +}); + +async function checkBadUrl(searchProviderKey, urlValue) { + let normalized = await ExtensionTestUtils.normalizeManifest({ + chrome_settings_overrides: { + search_provider: { + name: "MozSearch", + keyword: "MozSearch", + search_url: "https://example.com/", + [searchProviderKey]: urlValue, + }, + }, + }); + + ok( + /Error processing chrome_settings_overrides\.search_provider[^:]*: .* must match/.test( + normalized.error + ), + `Expected error for ${searchProviderKey}:${urlValue} "${normalized.error}"` + ); +} + +async function checkValidUrl(urlValue) { + let normalized = await ExtensionTestUtils.normalizeManifest({ + chrome_settings_overrides: { + search_provider: { + name: "MozSearch", + keyword: "MozSearch", + search_form: urlValue, + search_url: urlValue, + suggest_url: urlValue, + }, + }, + }); + equal(normalized.error, undefined, `Valid search_provider url: ${urlValue}`); +} + +add_task(async function test_extension_not_allow_http() { + await checkBadUrl("search_form", "http://example.com/{searchTerms}"); + await checkBadUrl("search_url", "http://example.com/{searchTerms}"); + await checkBadUrl("suggest_url", "http://example.com/{searchTerms}"); +}); + +add_task(async function test_manifest_disallows_http_localhost_prefix() { + await checkBadUrl("search_url", "http://localhost.example.com"); + await checkBadUrl("search_url", "http://localhost.example.com/"); + await checkBadUrl("search_url", "http://127.0.0.1.example.com/"); + await checkBadUrl("search_url", "http://localhost:1234@example.com/"); +}); + +add_task(async function test_manifest_allow_http_for_localhost() { + await checkValidUrl("http://localhost"); + await checkValidUrl("http://localhost/"); + await checkValidUrl("http://localhost:/"); + await checkValidUrl("http://localhost:1/"); + await checkValidUrl("http://localhost:65535/"); + + await checkValidUrl("http://127.0.0.1"); + await checkValidUrl("http://127.0.0.1:"); + await checkValidUrl("http://127.0.0.1:/"); + await checkValidUrl("http://127.0.0.1/"); + await checkValidUrl("http://127.0.0.1:80/"); + + await checkValidUrl("http://[::1]"); + await checkValidUrl("http://[::1]:"); + await checkValidUrl("http://[::1]:/"); + await checkValidUrl("http://[::1]/"); + await checkValidUrl("http://[::1]:80/"); +}); + +add_task(async function test_extension_allow_http_for_localhost() { + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + chrome_settings_overrides: { + search_provider: { + name: "MozSearch", + keyword: "MozSearch", + search_url: "http://localhost/{searchTerms}", + suggest_url: "http://localhost/suggest/{searchTerms}", + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + await AddonTestUtils.waitForSearchProviderStartup(ext1); + + let engine = Services.search.getEngineByName("MozSearch"); + ok(engine, "Engine should exist."); + + await ext1.unload(); +}); + +add_task(async function test_search_favicon_mv3() { + Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); + let normalized = await ExtensionTestUtils.normalizeManifest({ + manifest_version: 3, + chrome_settings_overrides: { + search_provider: { + name: "HTTP Icon in MV3", + search_url: "https://example.org/", + favicon_url: "https://example.org/icon.png", + }, + }, + }); + Assert.ok( + normalized.error.endsWith("must be a relative URL"), + "Should have an error" + ); + normalized = await ExtensionTestUtils.normalizeManifest({ + manifest_version: 3, + chrome_settings_overrides: { + search_provider: { + name: "HTTP Icon in MV3", + search_url: "https://example.org/", + favicon_url: "/icon.png", + }, + }, + }); + Assert.ok(!normalized.error, "Should not have an error"); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search_mozParam.js b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search_mozParam.js new file mode 100644 index 0000000000..3248c5cefa --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search_mozParam.js @@ -0,0 +1,239 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); +const { NimbusFeatures } = ChromeUtils.importESModule( + "resource://nimbus/ExperimentAPI.sys.mjs" +); +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +let { promiseShutdownManager, promiseStartupManager } = AddonTestUtils; + +// Note: these lists should be kept in sync with the lists in +// browser/components/extensions/test/xpcshell/data/test/manifest.json +// These params are conditional based on how search is initiated. +const mozParams = [ + { + name: "test-0", + condition: "purpose", + purpose: "contextmenu", + value: "0", + }, + { name: "test-1", condition: "purpose", purpose: "searchbar", value: "1" }, + { name: "test-2", condition: "purpose", purpose: "homepage", value: "2" }, + { name: "test-3", condition: "purpose", purpose: "keyword", value: "3" }, + { name: "test-4", condition: "purpose", purpose: "newtab", value: "4" }, +]; +// These params are always included. +const params = [ + { name: "simple", value: "5" }, + { name: "term", value: "{searchTerms}" }, + { name: "lang", value: "{language}" }, + { name: "locale", value: "{moz:locale}" }, + { name: "prefval", condition: "pref", pref: "code" }, +]; + +add_task(async function setup() { + let readyStub = sinon.stub(NimbusFeatures.search, "ready").resolves(); + let updateStub = sinon.stub(NimbusFeatures.search, "onUpdate"); + await promiseStartupManager(); + await SearchTestUtils.useTestEngines("data", null, [ + { + webExtension: { + id: "test@search.mozilla.org", + }, + appliesTo: [ + { + included: { everywhere: true }, + default: "yes", + }, + ], + }, + ]); + await Services.search.init(); + registerCleanupFunction(async () => { + await promiseShutdownManager(); + readyStub.restore(); + updateStub.restore(); + }); +}); + +/* This tests setting moz params. */ +add_task(async function test_extension_setting_moz_params() { + let defaultBranch = Services.prefs.getDefaultBranch("browser.search."); + defaultBranch.setCharPref("param.code", "good"); + + let engine = Services.search.getEngineByName("MozParamsTest"); + + let extraParams = []; + for (let p of params) { + if (p.condition == "pref") { + extraParams.push(`${p.name}=good`); + } else if (p.value == "{searchTerms}") { + extraParams.push(`${p.name}=test`); + } else if (p.value == "{language}") { + extraParams.push(`${p.name}=${Services.locale.requestedLocale || "*"}`); + } else if (p.value == "{moz:locale}") { + extraParams.push(`${p.name}=${Services.locale.requestedLocale}`); + } else { + extraParams.push(`${p.name}=${p.value}`); + } + } + let paramStr = extraParams.join("&"); + + for (let p of mozParams) { + let expectedURL = engine.getSubmission( + "test", + null, + p.condition == "purpose" ? p.purpose : null + ).uri.spec; + equal( + expectedURL, + `https://example.com/?q=test&${p.name}=${p.value}&${paramStr}`, + "search url is expected" + ); + } + + defaultBranch.setCharPref("param.code", ""); +}); + +add_task(async function test_nimbus_params() { + let sandbox = sinon.createSandbox(); + let stub = sandbox.stub(NimbusFeatures.search, "getVariable"); + // These values should match the nimbusParams below and the data/test/manifest.json + // search engine configuration + stub.withArgs("extraParams").returns([ + { + key: "nimbus-key-1", + value: "nimbus-value-1", + }, + { + key: "nimbus-key-2", + value: "nimbus-value-2", + }, + ]); + + Assert.ok( + NimbusFeatures.search.onUpdate.called, + "Called to initialize the cache" + ); + + // Populate the cache with the `getVariable` mock values + NimbusFeatures.search.onUpdate.firstCall.args[0](); + + let engine = Services.search.getEngineByName("MozParamsTest"); + + // Note: these lists should be kept in sync with the lists in + // browser/components/extensions/test/xpcshell/data/test/manifest.json + // These params are conditional based on how search is initiated. + const nimbusParams = [ + { name: "experimenter-1", condition: "pref", pref: "nimbus-key-1" }, + { name: "experimenter-2", condition: "pref", pref: "nimbus-key-2" }, + ]; + const experimentCache = { + "nimbus-key-1": "nimbus-value-1", + "nimbus-key-2": "nimbus-value-2", + }; + + let extraParams = []; + for (let p of params) { + if (p.value == "{searchTerms}") { + extraParams.push(`${p.name}=test`); + } else if (p.value == "{language}") { + extraParams.push(`${p.name}=${Services.locale.requestedLocale || "*"}`); + } else if (p.value == "{moz:locale}") { + extraParams.push(`${p.name}=${Services.locale.requestedLocale}`); + } else if (p.condition !== "pref") { + // Ignoring pref parameters + extraParams.push(`${p.name}=${p.value}`); + } + } + for (let p of nimbusParams) { + if (p.condition == "pref") { + extraParams.push(`${p.name}=${experimentCache[p.pref]}`); + } + } + let paramStr = extraParams.join("&"); + for (let p of mozParams) { + let expectedURL = engine.getSubmission( + "test", + null, + p.condition == "purpose" ? p.purpose : null + ).uri.spec; + equal( + expectedURL, + `https://example.com/?q=test&${p.name}=${p.value}&${paramStr}`, + "search url is expected" + ); + } + + sandbox.restore(); +}); + +add_task(async function test_extension_setting_moz_params_fail() { + // Ensure that the test infra does not automatically make + // this privileged. + AddonTestUtils.usePrivilegedSignatures = false; + Services.prefs.setCharPref( + "extensions.installedDistroAddon.test@mochitest", + "" + ); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { id: "test1@mochitest" }, + }, + chrome_settings_overrides: { + search_provider: { + name: "MozParamsTest1", + search_url: "https://example.com/", + params: [ + { + name: "testParam", + condition: "purpose", + purpose: "contextmenu", + value: "0", + }, + { name: "prefval", condition: "pref", pref: "code" }, + { name: "q", value: "{searchTerms}" }, + ], + }, + }, + }, + useAddonManager: "permanent", + }); + await extension.startup(); + await AddonTestUtils.waitForSearchProviderStartup(extension); + equal( + extension.extension.isPrivileged, + false, + "extension is not priviledged" + ); + let engine = Services.search.getEngineByName("MozParamsTest1"); + let expectedURL = engine.getSubmission("test", null, "contextmenu").uri.spec; + equal( + expectedURL, + "https://example.com/?q=test", + "engine cannot have conditional or pref params" + ); + await extension.unload(); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_shutdown.js b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_shutdown.js new file mode 100644 index 0000000000..851efd6b2a --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_shutdown.js @@ -0,0 +1,109 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +// Lazily import ExtensionParent to allow AddonTestUtils.createAppInfo to +// override Services.appinfo. +ChromeUtils.defineESModuleGetters(this, { + ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs", +}); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +add_task(async function shutdown_during_search_provider_startup() { + await AddonTestUtils.promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + chrome_settings_overrides: { + search_provider: { + is_default: true, + name: "dummy name", + search_url: "https://example.com/", + }, + }, + }, + }); + + info("Starting up search extension"); + await extension.startup(); + let extStartPromise = AddonTestUtils.waitForSearchProviderStartup(extension, { + // Search provider registration is expected to be pending because the search + // service has not been initialized yet. + expectPending: true, + }); + + let initialized = false; + Services.search.promiseInitialized.then(() => { + initialized = true; + }); + + await extension.addon.disable(); + + info("Extension managed to shut down despite the uninitialized search"); + // Initialize search after extension shutdown to check that it does not cause + // any problems, and that the test can continue to test uninstall behavior. + Assert.ok(!initialized, "Search service should not have been initialized"); + + extension.addon.enable(); + await extension.awaitStartup(); + + // Check that uninstall is blocked until the search registration at startup + // has finished. This registration only finished once the search service is + // initialized. + let uninstallingPromise = new Promise(resolve => { + let Management = ExtensionParent.apiManager; + Management.on("uninstall", function listener(eventName, { id }) { + Management.off("uninstall", listener); + Assert.equal(id, extension.id, "Expected extension"); + resolve(); + }); + }); + + let extRestartPromise = AddonTestUtils.waitForSearchProviderStartup( + extension, + { + // Search provider registration is expected to be pending again, + // because the search service has still not been initialized yet. + expectPending: true, + } + ); + + let uninstalledPromise = extension.addon.uninstall(); + let uninstalled = false; + uninstalledPromise.then(() => { + uninstalled = true; + }); + + await uninstallingPromise; + Assert.ok(!uninstalled, "Uninstall should not be finished yet"); + Assert.ok(!initialized, "Search service should still be uninitialized"); + await Services.search.init(); + Assert.ok(initialized, "Search service should be initialized"); + + // After initializing the search service, the search provider registration + // promises should settle eventually. + + // Despite the interrupted startup, the promise should still resolve without + // an error. + await extStartPromise; + // The extension that is still active. The promise should just resolve. + await extRestartPromise; + + // After initializing the search service, uninstall should eventually finish. + await uninstalledPromise; + + await AddonTestUtils.promiseShutdownManager(); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_settings_validate.js b/browser/components/extensions/test/xpcshell/test_ext_settings_validate.js new file mode 100644 index 0000000000..2f0d36f6e8 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_settings_validate.js @@ -0,0 +1,193 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { AddonManager } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); + +const { AboutNewTab } = ChromeUtils.importESModule( + "resource:///modules/AboutNewTab.sys.mjs" +); + +// Lazy load to avoid having Services.appinfo cached first. +ChromeUtils.defineESModuleGetters(this, { + ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs", +}); + +const { HomePage } = ChromeUtils.importESModule( + "resource:///modules/HomePage.sys.mjs" +); + +AddonTestUtils.init(this); + +// Allow for unsigned addons. +AddonTestUtils.overrideCertDB(); + +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +add_task(async function test_settings_modules_not_loaded() { + await ExtensionParent.apiManager.lazyInit(); + // Test that no settings modules are loaded. + let modules = Array.from(ExtensionParent.apiManager.settingsModules); + ok(modules.length, "we have settings modules"); + for (let name of modules) { + ok( + !ExtensionParent.apiManager.getModule(name).loaded, + `${name} is not loaded` + ); + } +}); + +add_task(async function test_settings_validated() { + let defaultNewTab = AboutNewTab.newTabURL; + equal(defaultNewTab, "about:newtab", "Newtab url is default."); + let defaultHomepageURL = HomePage.get(); + equal(defaultHomepageURL, "about:home", "Home page url is default."); + + let xpi = await AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { id: "test@mochi" } }, + chrome_url_overrides: { + newtab: "/newtab", + }, + chrome_settings_overrides: { + homepage: "https://example.com/", + }, + }, + }); + let extension = ExtensionTestUtils.expectExtension("test@mochi"); + let file = await AddonTestUtils.manuallyInstall(xpi); + await AddonTestUtils.promiseStartupManager(); + await extension.awaitStartup(); + + equal( + HomePage.get(), + "https://example.com/", + "Home page url is extension controlled." + ); + ok( + AboutNewTab.newTabURL.endsWith("/newtab"), + "newTabURL is extension controlled." + ); + + await AddonTestUtils.promiseShutdownManager(); + // After shutdown, delete the xpi file. + Services.obs.notifyObservers(xpi, "flush-cache-entry"); + try { + file.remove(true); + } catch (e) { + ok(false, e); + } + await AddonTestUtils.cleanupTempXPIs(); + + // Restart everything, the ExtensionAddonObserver should handle updating state. + let prefChanged = TestUtils.waitForPrefChange("browser.startup.homepage"); + await AddonTestUtils.promiseStartupManager(); + await prefChanged; + + equal(HomePage.get(), defaultHomepageURL, "Home page url is default."); + equal(AboutNewTab.newTabURL, defaultNewTab, "newTabURL is reset to default."); + await AddonTestUtils.promiseShutdownManager(); +}); + +add_task(async function test_settings_validated_safemode() { + let defaultNewTab = AboutNewTab.newTabURL; + equal(defaultNewTab, "about:newtab", "Newtab url is default."); + let defaultHomepageURL = HomePage.get(); + equal(defaultHomepageURL, "about:home", "Home page url is default."); + + function isDefaultSettings(postfix) { + equal( + HomePage.get(), + defaultHomepageURL, + `Home page url is default ${postfix}.` + ); + equal( + AboutNewTab.newTabURL, + defaultNewTab, + `newTabURL is default ${postfix}.` + ); + } + + function isExtensionSettings(postfix) { + equal( + HomePage.get(), + "https://example.com/", + `Home page url is extension controlled ${postfix}.` + ); + ok( + AboutNewTab.newTabURL.endsWith("/newtab"), + `newTabURL is extension controlled ${postfix}.` + ); + } + + async function switchSafeMode(inSafeMode) { + await AddonTestUtils.promiseShutdownManager(); + AddonTestUtils.appInfo.inSafeMode = inSafeMode; + await AddonTestUtils.promiseStartupManager(); + return AddonManager.getAddonByID("test@mochi"); + } + + let xpi = await AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version: "1.0", + browser_specific_settings: { gecko: { id: "test@mochi" } }, + chrome_url_overrides: { + newtab: "/newtab", + }, + chrome_settings_overrides: { + homepage: "https://example.com/", + }, + }, + }); + let extension = ExtensionTestUtils.expectExtension("test@mochi"); + await AddonTestUtils.manuallyInstall(xpi); + await AddonTestUtils.promiseStartupManager(); + await extension.awaitStartup(); + + isExtensionSettings("on extension startup"); + + // Disable in safemode and verify settings are removed in normal mode. + let addon = await switchSafeMode(true); + await addon.disable(); + addon = await switchSafeMode(false); + isDefaultSettings("after disabling addon during safemode"); + + // Enable in safemode and verify settings are back in normal mode. + addon = await switchSafeMode(true); + await addon.enable(); + addon = await switchSafeMode(false); + isExtensionSettings("after enabling addon during safemode"); + + // Uninstall in safemode and verify settings are removed in normal mode. + addon = await switchSafeMode(true); + await addon.uninstall(); + addon = await switchSafeMode(false); + isDefaultSettings("after uninstalling addon during safemode"); + + await AddonTestUtils.promiseShutdownManager(); + await AddonTestUtils.cleanupTempXPIs(); +}); + +// There are more settings modules than used in this test file, they should have been +// loaded during the test extensions uninstall. Ensure that all settings modules have +// been loaded. +add_task(async function test_settings_modules_loaded() { + // Test that all settings modules are loaded. + let modules = Array.from(ExtensionParent.apiManager.settingsModules); + ok(modules.length, "we have settings modules"); + for (let name of modules) { + ok(ExtensionParent.apiManager.getModule(name).loaded, `${name} was loaded`); + } +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_topSites.js b/browser/components/extensions/test/xpcshell/test_ext_topSites.js new file mode 100644 index 0000000000..8064ade1e8 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_topSites.js @@ -0,0 +1,293 @@ +"use strict"; + +const { PlacesUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PlacesUtils.sys.mjs" +); +const { NewTabUtils } = ChromeUtils.importESModule( + "resource://gre/modules/NewTabUtils.sys.mjs" +); +const { PlacesTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PlacesTestUtils.sys.mjs" +); + +const SEARCH_SHORTCUTS_EXPERIMENT_PREF = + "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts"; + +// A small 1x1 test png +const IMAGE_1x1 = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg=="; + +add_task(async function test_topSites() { + Services.prefs.setBoolPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF, false); + let visits = []; + const numVisits = 15; // To make sure we get frecency. + let visitDate = new Date(1999, 9, 9, 9, 9).getTime(); + + async function setVisit(visit) { + for (let j = 0; j < numVisits; ++j) { + visitDate -= 1000; + visit.visits.push({ date: new Date(visitDate) }); + } + visits.push(visit); + await PlacesUtils.history.insert(visit); + } + // Stick a couple sites into history. + for (let i = 0; i < 2; ++i) { + await setVisit({ + url: `http://example${i}.com/`, + title: `visit${i}`, + visits: [], + }); + await setVisit({ + url: `http://www.example${i}.com/foobar`, + title: `visit${i}-www`, + visits: [], + }); + } + NewTabUtils.init(); + + // Insert a favicon to show that favicons are not returned by default. + let faviconData = new Map(); + faviconData.set("http://example0.com", IMAGE_1x1); + await PlacesTestUtils.addFavicons(faviconData); + + // Ensure our links show up in activityStream. + let links = await NewTabUtils.activityStreamLinks.getTopSites({ + onePerDomain: false, + topsiteFrecency: 1, + }); + + equal( + links.length, + visits.length, + "Top sites has been successfully initialized" + ); + + // Drop the visits.visits for later testing. + visits = visits.map(v => { + return { url: v.url, title: v.title, favicon: undefined, type: "url" }; + }); + + // Test that results from all providers are returned by default. + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["topSites"], + }, + background() { + browser.test.onMessage.addListener(async options => { + let sites = await browser.topSites.get(options); + browser.test.sendMessage("sites", sites); + }); + }, + }); + + await extension.startup(); + + function getSites(options) { + extension.sendMessage(options); + return extension.awaitMessage("sites"); + } + + Assert.deepEqual( + [visits[0], visits[2]], + await getSites(), + "got topSites default" + ); + Assert.deepEqual( + visits, + await getSites({ onePerDomain: false }), + "got topSites all links" + ); + + NewTabUtils.activityStreamLinks.blockURL(visits[0]); + ok( + NewTabUtils.blockedLinks.isBlocked(visits[0]), + `link ${visits[0].url} is blocked` + ); + + Assert.deepEqual( + [visits[2], visits[1]], + await getSites(), + "got topSites with blocked links filtered out" + ); + Assert.deepEqual( + [visits[0], visits[2]], + await getSites({ includeBlocked: true }), + "got topSites with blocked links included" + ); + + // Test favicon result + let topSites = await getSites({ includeBlocked: true, includeFavicon: true }); + equal(topSites[0].favicon, IMAGE_1x1, "received favicon"); + + equal( + 1, + (await getSites({ limit: 1, includeBlocked: true })).length, + "limit 1 topSite" + ); + + NewTabUtils.uninit(); + await extension.unload(); + await PlacesUtils.history.clear(); + Services.prefs.clearUserPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF); +}); + +// Test pinned likns and search shortcuts. +add_task(async function test_topSites_complete() { + Services.prefs.setBoolPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF, true); + NewTabUtils.init(); + let time = new Date(); + let pinnedIndex = 0; + let entries = [ + { + url: `http://pinned1.com/`, + title: "pinned1", + type: "url", + pinned: pinnedIndex++, + visitDate: time, + }, + { + url: `http://search1.com/`, + title: "@search1", + type: "search", + pinned: pinnedIndex++, + visitDate: new Date(--time), + }, + { + url: `https://amazon.com`, + title: "@amazon", + type: "search", + visitDate: new Date(--time), + }, + { + url: `http://history1.com/`, + title: "history1", + type: "url", + visitDate: new Date(--time), + }, + { + url: `http://history2.com/`, + title: "history2", + type: "url", + visitDate: new Date(--time), + }, + { + url: `https://blocked1.com/`, + title: "blocked1", + type: "blocked", + visitDate: new Date(--time), + }, + ]; + + for (let entry of entries) { + // Build up frecency. + await PlacesUtils.history.insert({ + url: entry.url, + title: entry.title, + visits: new Array(15).fill({ + date: entry.visitDate, + transition: PlacesUtils.history.TRANSITIONS.LINK, + }), + }); + // Insert a favicon to show that favicons are not returned by default. + await PlacesTestUtils.addFavicons(new Map([[entry.url, IMAGE_1x1]])); + if (entry.pinned !== undefined) { + let info = + entry.type == "search" + ? { url: entry.url, label: entry.title, searchTopSite: true } + : { url: entry.url, title: entry.title }; + NewTabUtils.pinnedLinks.pin(info, entry.pinned); + } + if (entry.type == "blocked") { + NewTabUtils.activityStreamLinks.blockURL({ url: entry.url }); + } + } + + // Some transformation is necessary to match output data. + let expectedResults = entries + .filter(e => e.type != "blocked") + .map(e => { + e.favicon = undefined; + delete e.visitDate; + delete e.pinned; + return e; + }); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["topSites"], + }, + background() { + browser.test.onMessage.addListener(async options => { + let sites = await browser.topSites.get(options); + browser.test.sendMessage("sites", sites); + }); + }, + }); + + await extension.startup(); + + // Test that results are returned by the API. + function getSites(options) { + extension.sendMessage(options); + return extension.awaitMessage("sites"); + } + + Assert.deepEqual( + expectedResults, + await getSites({ includePinned: true, includeSearchShortcuts: true }), + "got topSites all links" + ); + + // Test no shortcuts. + dump(JSON.stringify(await getSites({ includePinned: true })) + "\n"); + Assert.ok( + !(await getSites({ includePinned: true })).some( + link => link.type == "search" + ), + "should get no shortcuts" + ); + + // Test favicons. + let topSites = await getSites({ + includePinned: true, + includeSearchShortcuts: true, + includeFavicon: true, + }); + Assert.ok( + topSites.every(f => f.favicon == IMAGE_1x1), + "favicon is correct" + ); + + // Test options.limit. + Assert.equal( + 1, + ( + await getSites({ + includePinned: true, + includeSearchShortcuts: true, + limit: 1, + }) + ).length, + "limit to 1 topSite" + ); + + // Clear history for a pinned entry, then check results. + await PlacesUtils.history.remove("http://pinned1.com/"); + let links = await getSites({ includePinned: true }); + Assert.ok( + links.find(link => link.url == "http://pinned1.com/"), + "Check unvisited pinned links are returned." + ); + links = await getSites(); + Assert.ok( + !links.find(link => link.url == "http://pinned1.com/"), + "Check unvisited pinned links are not returned." + ); + + await extension.unload(); + NewTabUtils.uninit(); + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + Services.prefs.clearUserPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab.js b/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab.js new file mode 100644 index 0000000000..9ea6c4eea6 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab.js @@ -0,0 +1,340 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + Management: "resource://gre/modules/Extension.sys.mjs", +}); + +const { AboutNewTab } = ChromeUtils.importESModule( + "resource:///modules/AboutNewTab.sys.mjs" +); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { + createAppInfo, + promiseRestartManager, + promiseShutdownManager, + promiseStartupManager, +} = AddonTestUtils; + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42", "42"); + +function awaitEvent(eventName) { + return new Promise(resolve => { + Management.once(eventName, (e, ...args) => resolve(...args)); + }); +} + +const DEFAULT_NEW_TAB_URL = AboutNewTab.newTabURL; + +add_task(async function test_multiple_extensions_overriding_newtab_page() { + const NEWTAB_URI_2 = "webext-newtab-1.html"; + const NEWTAB_URI_3 = "webext-newtab-2.html"; + const EXT_2_ID = "ext2@tests.mozilla.org"; + const EXT_3_ID = "ext3@tests.mozilla.org"; + + const CONTROLLED_BY_THIS = "controlled_by_this_extension"; + const CONTROLLED_BY_OTHER = "controlled_by_other_extensions"; + const NOT_CONTROLLABLE = "not_controllable"; + + const NEW_TAB_PRIVATE_ALLOWED = "browser.newtab.privateAllowed"; + const NEW_TAB_EXTENSION_CONTROLLED = "browser.newtab.extensionControlled"; + + function background() { + browser.test.onMessage.addListener(async msg => { + switch (msg) { + case "checkNewTabPage": + let newTabPage = await browser.browserSettings.newTabPageOverride.get( + {} + ); + browser.test.sendMessage("newTabPage", newTabPage); + break; + case "trySet": + let setResult = await browser.browserSettings.newTabPageOverride.set({ + value: "foo", + }); + browser.test.assertFalse( + setResult, + "Calling newTabPageOverride.set returns false." + ); + browser.test.sendMessage("newTabPageSet"); + break; + case "tryClear": + let clearResult = + await browser.browserSettings.newTabPageOverride.clear({}); + browser.test.assertFalse( + clearResult, + "Calling newTabPageOverride.clear returns false." + ); + browser.test.sendMessage("newTabPageCleared"); + break; + } + }); + } + + async function checkNewTabPageOverride( + ext, + expectedValue, + expectedLevelOfControl + ) { + ext.sendMessage("checkNewTabPage"); + let newTabPage = await ext.awaitMessage("newTabPage"); + + ok( + newTabPage.value.endsWith(expectedValue), + `newTabPageOverride setting returns the expected value ending with: ${expectedValue}.` + ); + equal( + newTabPage.levelOfControl, + expectedLevelOfControl, + `newTabPageOverride setting returns the expected levelOfControl: ${expectedLevelOfControl}.` + ); + } + + function verifyNewTabSettings(ext, expectedLevelOfControl) { + if (expectedLevelOfControl !== NOT_CONTROLLABLE) { + // Verify the preferences are set as expected. + let policy = WebExtensionPolicy.getByID(ext.id); + equal( + policy && policy.privateBrowsingAllowed, + Services.prefs.getBoolPref(NEW_TAB_PRIVATE_ALLOWED), + "private browsing flag set correctly" + ); + ok( + Services.prefs.getBoolPref(NEW_TAB_EXTENSION_CONTROLLED), + `extension controlled flag set correctly` + ); + } else { + ok( + !Services.prefs.prefHasUserValue(NEW_TAB_PRIVATE_ALLOWED), + "controlled flag reset" + ); + ok( + !Services.prefs.prefHasUserValue(NEW_TAB_EXTENSION_CONTROLLED), + "controlled flag reset" + ); + } + } + + let extObj = { + manifest: { + chrome_url_overrides: {}, + permissions: ["browserSettings"], + }, + useAddonManager: "temporary", + background, + }; + + let ext1 = ExtensionTestUtils.loadExtension(extObj); + + extObj.manifest.chrome_url_overrides = { newtab: NEWTAB_URI_2 }; + extObj.manifest.browser_specific_settings = { gecko: { id: EXT_2_ID } }; + let ext2 = ExtensionTestUtils.loadExtension(extObj); + + extObj.manifest.chrome_url_overrides = { newtab: NEWTAB_URI_3 }; + extObj.manifest.browser_specific_settings.gecko.id = EXT_3_ID; + extObj.incognitoOverride = "spanning"; + let ext3 = ExtensionTestUtils.loadExtension(extObj); + + equal( + AboutNewTab.newTabURL, + DEFAULT_NEW_TAB_URL, + "newTabURL is set to the default." + ); + + await promiseStartupManager(); + + await ext1.startup(); + equal( + AboutNewTab.newTabURL, + DEFAULT_NEW_TAB_URL, + "newTabURL is still set to the default." + ); + + await checkNewTabPageOverride(ext1, AboutNewTab.newTabURL, NOT_CONTROLLABLE); + verifyNewTabSettings(ext1, NOT_CONTROLLABLE); + + await ext2.startup(); + ok( + AboutNewTab.newTabURL.endsWith(NEWTAB_URI_2), + "newTabURL is overridden by the second extension." + ); + await checkNewTabPageOverride(ext1, NEWTAB_URI_2, CONTROLLED_BY_OTHER); + verifyNewTabSettings(ext2, CONTROLLED_BY_THIS); + + // Verify that calling set and clear do nothing. + ext2.sendMessage("trySet"); + await ext2.awaitMessage("newTabPageSet"); + await checkNewTabPageOverride(ext1, NEWTAB_URI_2, CONTROLLED_BY_OTHER); + verifyNewTabSettings(ext2, CONTROLLED_BY_THIS); + + ext2.sendMessage("tryClear"); + await ext2.awaitMessage("newTabPageCleared"); + await checkNewTabPageOverride(ext1, NEWTAB_URI_2, CONTROLLED_BY_OTHER); + verifyNewTabSettings(ext2, CONTROLLED_BY_THIS); + + // Disable the second extension. + let addon = await AddonManager.getAddonByID(EXT_2_ID); + let disabledPromise = awaitEvent("shutdown"); + await addon.disable(); + await disabledPromise; + equal( + AboutNewTab.newTabURL, + DEFAULT_NEW_TAB_URL, + "newTabURL url is reset to the default after second extension is disabled." + ); + await checkNewTabPageOverride(ext1, AboutNewTab.newTabURL, NOT_CONTROLLABLE); + verifyNewTabSettings(ext1, NOT_CONTROLLABLE); + + // Re-enable the second extension. + let enabledPromise = awaitEvent("ready"); + await addon.enable(); + await enabledPromise; + ok( + AboutNewTab.newTabURL.endsWith(NEWTAB_URI_2), + "newTabURL is overridden by the second extension." + ); + await checkNewTabPageOverride(ext2, NEWTAB_URI_2, CONTROLLED_BY_THIS); + verifyNewTabSettings(ext2, CONTROLLED_BY_THIS); + + await ext1.unload(); + ok( + AboutNewTab.newTabURL.endsWith(NEWTAB_URI_2), + "newTabURL is still overridden by the second extension." + ); + await checkNewTabPageOverride(ext2, NEWTAB_URI_2, CONTROLLED_BY_THIS); + verifyNewTabSettings(ext2, CONTROLLED_BY_THIS); + + await ext3.startup(); + ok( + AboutNewTab.newTabURL.endsWith(NEWTAB_URI_3), + "newTabURL is overridden by the third extension." + ); + await checkNewTabPageOverride(ext2, NEWTAB_URI_3, CONTROLLED_BY_OTHER); + verifyNewTabSettings(ext3, CONTROLLED_BY_THIS); + + // Disable the second extension. + disabledPromise = awaitEvent("shutdown"); + await addon.disable(); + await disabledPromise; + ok( + AboutNewTab.newTabURL.endsWith(NEWTAB_URI_3), + "newTabURL is still overridden by the third extension." + ); + await checkNewTabPageOverride(ext3, NEWTAB_URI_3, CONTROLLED_BY_THIS); + verifyNewTabSettings(ext3, CONTROLLED_BY_THIS); + + // Re-enable the second extension. + enabledPromise = awaitEvent("ready"); + await addon.enable(); + await enabledPromise; + ok( + AboutNewTab.newTabURL.endsWith(NEWTAB_URI_3), + "newTabURL is still overridden by the third extension." + ); + await checkNewTabPageOverride(ext3, NEWTAB_URI_3, CONTROLLED_BY_THIS); + verifyNewTabSettings(ext3, CONTROLLED_BY_THIS); + + await ext3.unload(); + ok( + AboutNewTab.newTabURL.endsWith(NEWTAB_URI_2), + "newTabURL reverts to being overridden by the second extension." + ); + await checkNewTabPageOverride(ext2, NEWTAB_URI_2, CONTROLLED_BY_THIS); + verifyNewTabSettings(ext2, CONTROLLED_BY_THIS); + + await ext2.unload(); + equal( + AboutNewTab.newTabURL, + DEFAULT_NEW_TAB_URL, + "newTabURL url is reset to the default." + ); + ok( + !Services.prefs.prefHasUserValue(NEW_TAB_PRIVATE_ALLOWED), + "controlled flag reset" + ); + ok( + !Services.prefs.prefHasUserValue(NEW_TAB_EXTENSION_CONTROLLED), + "controlled flag reset" + ); + + await promiseShutdownManager(); +}); + +// Tests that we handle the upgrade/downgrade process correctly +// when an extension is installed temporarily on top of a permanently +// installed one. +add_task(async function test_temporary_installation() { + const ID = "newtab@tests.mozilla.org"; + const PAGE1 = "page1.html"; + const PAGE2 = "page2.html"; + + equal( + AboutNewTab.newTabURL, + DEFAULT_NEW_TAB_URL, + "newTabURL is set to the default." + ); + + await promiseStartupManager(); + + let permanent = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { id: ID }, + }, + chrome_url_overrides: { + newtab: PAGE1, + }, + }, + useAddonManager: "permanent", + }); + + await permanent.startup(); + ok( + AboutNewTab.newTabURL.endsWith(PAGE1), + "newTabURL is overridden by permanent extension." + ); + + let temporary = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { id: ID }, + }, + chrome_url_overrides: { + newtab: PAGE2, + }, + }, + useAddonManager: "temporary", + }); + + await temporary.startup(); + ok( + AboutNewTab.newTabURL.endsWith(PAGE2), + "newTabURL is overridden by temporary extension." + ); + + await promiseRestartManager(); + await permanent.awaitStartup(); + + ok( + AboutNewTab.newTabURL.endsWith(PAGE1), + "newTabURL is back to the value set by permanent extension." + ); + + await permanent.unload(); + + equal( + AboutNewTab.newTabURL, + DEFAULT_NEW_TAB_URL, + "newTabURL is set back to the default." + ); + await promiseShutdownManager(); +}); diff --git a/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab_update.js b/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab_update.js new file mode 100644 index 0000000000..17ee81e5ef --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab_update.js @@ -0,0 +1,127 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { AboutNewTab } = ChromeUtils.importESModule( + "resource:///modules/AboutNewTab.sys.mjs" +); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { + createAppInfo, + createTempWebExtensionFile, + promiseCompleteAllInstalls, + promiseFindAddonUpdates, + promiseShutdownManager, + promiseStartupManager, +} = AddonTestUtils; + +AddonTestUtils.init(this); + +// Allow for unsigned addons. +AddonTestUtils.overrideCertDB(); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42", "42"); + +add_task(async function test_url_overrides_newtab_update() { + const EXTENSION_ID = "test_url_overrides_update@tests.mozilla.org"; + const NEWTAB_URI = "webext-newtab-1.html"; + const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity"; + + const testServer = createHttpServer(); + const port = testServer.identity.primaryPort; + + // The test extension uses an insecure update url. + Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); + + testServer.registerPathHandler("/test_update.json", (request, response) => { + response.write(`{ + "addons": { + "${EXTENSION_ID}": { + "updates": [ + { + "version": "2.0", + "update_link": "http://localhost:${port}/addons/test_url_overrides-2.0.xpi" + } + ] + } + } + }`); + }); + + let webExtensionFile = createTempWebExtensionFile({ + manifest: { + version: "2.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }, + }); + + testServer.registerFile( + "/addons/test_url_overrides-2.0.xpi", + webExtensionFile + ); + + await promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + update_url: `http://localhost:${port}/test_update.json`, + }, + }, + chrome_url_overrides: { newtab: NEWTAB_URI }, + }, + }); + + let defaultNewTabURL = AboutNewTab.newTabURL; + equal( + AboutNewTab.newTabURL, + defaultNewTabURL, + `Default newtab url is ${defaultNewTabURL}.` + ); + + await extension.startup(); + + equal( + extension.version, + "1.0", + "The installed addon has the expected version." + ); + ok( + AboutNewTab.newTabURL.endsWith(NEWTAB_URI), + "Newtab url is overridden by the extension." + ); + + let update = await promiseFindAddonUpdates(extension.addon); + let install = update.updateAvailable; + + await promiseCompleteAllInstalls([install]); + + await extension.awaitStartup(); + + equal( + extension.version, + "2.0", + "The updated addon has the expected version." + ); + equal( + AboutNewTab.newTabURL, + defaultNewTabURL, + "Newtab url reverted to the default after update." + ); + + await extension.unload(); + + await promiseShutdownManager(); +}); diff --git a/browser/components/extensions/test/xpcshell/xpcshell.toml b/browser/components/extensions/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..e98f696264 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/xpcshell.toml @@ -0,0 +1,69 @@ +[DEFAULT] +skip-if = ["os == 'android'"] # bug 1730213 +head = "head.js" +firefox-appdir = "browser" +tags = "webextensions condprof" +dupe-manifest = "" + +["test_ext_bookmarks.js"] +skip-if = ["condprof"] # Bug 1769184 - by design for now + +["test_ext_browsingData_downloads.js"] + +["test_ext_browsingData_passwords.js"] +skip-if = ["tsan"] # Times out, bug 1612707 + +["test_ext_browsingData_settings.js"] + +["test_ext_chrome_settings_overrides_home.js"] + +["test_ext_chrome_settings_overrides_update.js"] + +["test_ext_distribution_popup.js"] + +["test_ext_history.js"] + +["test_ext_homepage_overrides_private.js"] + +["test_ext_manifest.js"] + +["test_ext_manifest_commands.js"] +run-sequentially = "very high failure rate in parallel" + +["test_ext_manifest_omnibox.js"] + +["test_ext_manifest_permissions.js"] + +["test_ext_menu_caller.js"] + +["test_ext_menu_startup.js"] + +["test_ext_normandyAddonStudy.js"] + +["test_ext_pageAction_shutdown.js"] + +["test_ext_pkcs11_management.js"] + +["test_ext_settings_overrides_defaults.js"] +skip-if = ["condprof"] # Bug 1776135 - by design, modifies search settings at start of test +support-files = [ + "data/test/manifest.json", + "data/test2/manifest.json", +] + +["test_ext_settings_overrides_search.js"] + +["test_ext_settings_overrides_search_mozParam.js"] +skip-if = ["condprof"] # Bug 1776652 +support-files = ["data/test/manifest.json"] + +["test_ext_settings_overrides_shutdown.js"] + +["test_ext_settings_validate.js"] + +["test_ext_topSites.js"] +skip-if = ["condprof"] # Bug 1769184 - by design for now + +["test_ext_url_overrides_newtab.js"] + +["test_ext_url_overrides_newtab_update.js"] -- cgit v1.2.3