diff options
Diffstat (limited to 'toolkit/components/search/tests/xpcshell')
213 files changed, 22920 insertions, 0 deletions
diff --git a/toolkit/components/search/tests/xpcshell/data/bigIcon.ico b/toolkit/components/search/tests/xpcshell/data/bigIcon.ico Binary files differnew file mode 100644 index 0000000000..f22522411d --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/bigIcon.ico diff --git a/toolkit/components/search/tests/xpcshell/data/engine-app/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-app/manifest.json new file mode 100644 index 0000000000..14bc27bdd4 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-app/manifest.json @@ -0,0 +1,24 @@ +{ + "name": "TestEngineApp", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine-app@search.mozilla.org" + } + }, + "hidden": true, + "description": "A test search engine installed in the application directory", + "chrome_settings_overrides": { + "search_provider": { + "name": "TestEngineApp", + "search_url": "https://localhost/", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-chromeicon/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-chromeicon/manifest.json new file mode 100644 index 0000000000..d2896b78f8 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-chromeicon/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "engine-chromeicon", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine-chromeicon@search.mozilla.org" + } + }, + "hidden": true, + "chrome_settings_overrides": { + "search_provider": { + "name": "engine-chromeicon", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-diff-name/_locales/en/messages.json b/toolkit/components/search/tests/xpcshell/data/engine-diff-name/_locales/en/messages.json new file mode 100644 index 0000000000..07580b8ea5 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-diff-name/_locales/en/messages.json @@ -0,0 +1,8 @@ +{ + "extensionName": { + "message": "engine-diff-name-en" + }, + "searchUrl": { + "message": "https://en.wikipedia.com/search" + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-diff-name/_locales/gd/messages.json b/toolkit/components/search/tests/xpcshell/data/engine-diff-name/_locales/gd/messages.json new file mode 100644 index 0000000000..de01d16bf0 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-diff-name/_locales/gd/messages.json @@ -0,0 +1,8 @@ +{ + "extensionName": { + "message": "engine-diff-name-gd" + }, + "searchUrl": { + "message": "https://gd.wikipedia.com/search" + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-diff-name/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-diff-name/manifest.json new file mode 100644 index 0000000000..3c80765f61 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-diff-name/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "engine-diff-name", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine-diff-name@search.mozilla.org" + } + }, + "hidden": true, + "icons": { + "16": "favicon.png" + }, + "default_locale": "en", + "chrome_settings_overrides": { + "search_provider": { + "name": "__MSG_extensionName__", + "search_url": "__MSG_searchUrl__" + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-fr.xml b/toolkit/components/search/tests/xpcshell/data/engine-fr.xml new file mode 100644 index 0000000000..4bb4426a12 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-fr.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Test search engine (fr)</ShortName>
+<Description>A test search engine (based on Google search for a different locale)</Description>
+<InputEncoding>ISO-8859-1</InputEncoding>
+<Url type="text/html" method="GET" template="http://www.google.fr/search">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="ie" value="iso-8859-1"/>
+ <Param name="oe" value="iso-8859-1"/>
+</Url>
+<SearchForm>http://www.google.fr/</SearchForm>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-fr/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-fr/manifest.json new file mode 100644 index 0000000000..cc895d26d9 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-fr/manifest.json @@ -0,0 +1,32 @@ +{ + "name": "Test search engine (fr)", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine-fr@search.mozilla.org" + } + }, + "hidden": true, + "description": "A test search engine (based on Google search for a different locale)", + "chrome_settings_overrides": { + "search_provider": { + "name": "Test search engine (fr)", + "search_url": "https://www.google.fr/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "ie", + "value": "iso-8859-1" + }, + { + "name": "oe", + "value": "iso-8859-1" + } + ] + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-override/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-override/manifest.json new file mode 100644 index 0000000000..a894be0a41 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-override/manifest.json @@ -0,0 +1,24 @@ +{ + "name": "bug645970", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine-override@search.mozilla.org" + } + }, + "hidden": true, + "description": "override", + "chrome_settings_overrides": { + "search_provider": { + "name": "bug645970", + "search_url": "https://searchtest.local", + "params": [ + { + "name": "search", + "value": "{searchTerms}" + } + ] + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-pref/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-pref/manifest.json new file mode 100644 index 0000000000..d796b65b63 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-pref/manifest.json @@ -0,0 +1,33 @@ +{ + "name": "engine-pref", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine-pref@search.mozilla.org" + } + }, + "hidden": true, + "chrome_settings_overrides": { + "search_provider": { + "name": "engine-pref", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "code", + "condition": "pref", + "pref": "code" + }, + { + "name": "test", + "condition": "pref", + "pref": "test" + } + ] + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-purposes/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-purposes/manifest.json new file mode 100644 index 0000000000..fc709063e1 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-purposes/manifest.json @@ -0,0 +1,62 @@ +{ + "name": "Test Engine With Purposes", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine-purposes@search.mozilla.org" + } + }, + "description": "A test search engine with purposes", + "chrome_settings_overrides": { + "search_provider": { + "name": "Test Engine With Purposes", + "search_url": "https://www.example.com/search", + "params": [ + { + "name": "form", + "condition": "purpose", + "purpose": "keyword", + "value": "MOZKEYWORD" + }, + { + "name": "form", + "condition": "purpose", + "purpose": "contextmenu", + "value": "MOZCONTEXT" + }, + { + "name": "form", + "condition": "purpose", + "purpose": "newtab", + "value": "MOZNEWTAB" + }, + { + "name": "form", + "condition": "purpose", + "purpose": "searchbar", + "value": "MOZSEARCHBAR" + }, + { + "name": "form", + "condition": "purpose", + "purpose": "homepage", + "value": "MOZHOMEPAGE" + }, + { + "name": "pc", + "value": "FIREFOX" + }, + { + "name": "channel", + "condition": "pref", + "pref": "testChannelEnabled" + }, + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-purpose/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-purpose/manifest.json new file mode 100644 index 0000000000..ed4a609e7c --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-purpose/manifest.json @@ -0,0 +1,41 @@ +{ + "name": "engine-rel-searchform-purpose", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine-rel-searchform-purpose@search.mozilla.org" + } + }, + "hidden": true, + "chrome_settings_overrides": { + "search_provider": { + "name": "engine-rel-searchform-purpose", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "contextmenu", + "value": "rcs" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "keyword", + "value": "fflb" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "searchbar", + "value": "sb" + } + ] + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-reordered/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-reordered/manifest.json new file mode 100644 index 0000000000..cc3fc95430 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-reordered/manifest.json @@ -0,0 +1,40 @@ +{ + "name": "Test search engine (Reordered)", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine-reordered@search.mozilla.org" + } + }, + "hidden": true, + "description": "A test search engine (based on Google search)", + "icons": { + "16": "favicon.ico" + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "Test search engine (Reordered)", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "contextmenu", + "value": "rcs" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "keyword", + "value": "fflb" + } + ], + "suggest_url": "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}" + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/_locales/en/messages.json b/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/_locales/en/messages.json new file mode 100644 index 0000000000..1cc3f68ee1 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/_locales/en/messages.json @@ -0,0 +1,8 @@ +{ + "extensionName": { + "message": "engine-resourceicon" + }, + "searchUrl": { + "message": "https://www.google.com/search" + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/_locales/gd/messages.json b/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/_locales/gd/messages.json new file mode 100644 index 0000000000..3c02e6a2af --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/_locales/gd/messages.json @@ -0,0 +1,8 @@ +{ + "extensionName": { + "message": "engine-resourceicon-gd" + }, + "searchUrl": { + "message": "https://www.google.com/search" + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/manifest.json new file mode 100644 index 0000000000..dc62336145 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-resourceicon/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "engine-resourceicon", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine-resourceicon@search.mozilla.org" + } + }, + "hidden": true, + "icons": { + "16": "favicon.png" + }, + "default_locale": "en", + "chrome_settings_overrides": { + "search_provider": { + "name": "__MSG_extensionName__", + "search_url": "__MSG_searchUrl__" + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-same-name/_locales/en/messages.json b/toolkit/components/search/tests/xpcshell/data/engine-same-name/_locales/en/messages.json new file mode 100644 index 0000000000..ee808e7a62 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-same-name/_locales/en/messages.json @@ -0,0 +1,8 @@ +{ + "extensionName": { + "message": "engine-same-name" + }, + "searchUrl": { + "message": "https://www.google.com/search?q={searchTerms}" + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-same-name/_locales/gd/messages.json b/toolkit/components/search/tests/xpcshell/data/engine-same-name/_locales/gd/messages.json new file mode 100644 index 0000000000..476a9e56cc --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-same-name/_locales/gd/messages.json @@ -0,0 +1,8 @@ +{ + "extensionName": { + "message": "engine-same-name" + }, + "searchUrl": { + "message": "https://www.example.com/search?q={searchTerms}" + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-same-name/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-same-name/manifest.json new file mode 100644 index 0000000000..dc8a4c5d45 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-same-name/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "engine-same-name", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine-same-name@search.mozilla.org" + } + }, + "hidden": true, + "icons": { + "16": "favicon.png" + }, + "default_locale": "en", + "chrome_settings_overrides": { + "search_provider": { + "name": "__MSG_extensionName__", + "search_url": "__MSG_searchUrl__" + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine-system-purpose/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine-system-purpose/manifest.json new file mode 100644 index 0000000000..d268af8eab --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-system-purpose/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "engine-system-purpose", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine-system-purpose@search.mozilla.org" + } + }, + "hidden": true, + "chrome_settings_overrides": { + "search_provider": { + "name": "engine-system-purpose", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "searchbar", + "value": "sb" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "system", + "value": "sys" + } + ] + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine.xml b/toolkit/components/search/tests/xpcshell/data/engine.xml new file mode 100644 index 0000000000..a665e46b0b --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>Test search engine</ShortName> +<Description>A test search engine (based on Google search)</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA</Image> +<Url type="application/x-suggestions+json" method="GET" template="https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}"/> +<Url type="text/html" method="GET" template="https://www.google.com/search"> + <Param name="q" value="{searchTerms}"/> + <!-- Dynamic parameters --> + <MozParam name="channel" condition="purpose" purpose="contextmenu" value="rcs"/> + <MozParam name="channel" condition="purpose" purpose="keyword" value="fflb"/> +</Url> +<SearchForm>http://www.google.com/</SearchForm> +</SearchPlugin> diff --git a/toolkit/components/search/tests/xpcshell/data/engine/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine/manifest.json new file mode 100644 index 0000000000..30d221f388 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine/manifest.json @@ -0,0 +1,40 @@ +{ + "name": "Test search engine", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine@search.mozilla.org" + } + }, + "hidden": true, + "description": "A test search engine (based on Google search)", + "icons": { + "16": "favicon.ico" + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "Test search engine", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "contextmenu", + "value": "rcs" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "keyword", + "value": "fflb" + } + ], + "suggest_url": "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}" + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engine2.xml b/toolkit/components/search/tests/xpcshell/data/engine2.xml new file mode 100644 index 0000000000..9957bfdf48 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine2.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>A second test engine</ShortName> + <Description>A second test search engine (based on DuckDuckGo)</Description> + <InputEncoding>UTF-8</InputEncoding> + <LongName>A second test search engine (based on DuckDuckGo)</LongName> + <Image width="16" height="16">data:image/vnd.microsoft.icon;base64,AAABAAMAEBAAAAEACABoBQAANgAAACAgAAABAAgAqAgAAJ4FAAAwMAAAAQAIAKgOAABGDgAAKAAAABAAAAAgAAAAAQAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUvgAAFMAAABfDAAAYwQAAGscAAB7KAAAgzAAAIcwAACHNAAAizgAAI9AAACTQAAAnzAAAJtMAACnXABktyAAALtgAAC/bAAAx3QAAM+AAADbjAAA34wAAOOQAADzYAAA65wAWPdcAADvoAABG1QC6cg0AAFXdAERS0AAAVd8AAF3cAABp5wAAcOUATGziAAB36AAAeugATHLqAF165ABlg+0A0qRkAACS7wAAlO8AeI7nAIGX6gCVndwAAKX1AACr9gAAr/YAALX4AAC3+QCdrvIAALz6AAC9+QAAv/kAAMD7AADH+wAAyvwAAND9ALHB9QAA0vwA5c68AADT/gAA1P0AANT+AADU/wAA1/8AANj/AADZ/wAA3P8AAN3/AGrV+wAA3/8AAOH/AMrS9ADs3tYA39zmAOHj8AB+6v8A5ur6AKDw/wCq8f8A9fDtAPby7wD38vAA6/D8APfz8QD69O4A8PP8APL0/AD69/IA+/fzAPr39QDd+f8A+/n1APb4/QD7+fgA+/r5AOz8/gD1/P8A+vz/AP7+/QD//v8A////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABpLGhoaGhQHgEAAQEBAQFpGUtoaGhoSw8CAwICAgICAiNQaGhoaC0EBAwgIBsEBAQnWmhoaGgnIjY5PUBHOyUFLWhoaGheR0MdBgYGByQrCEtoaGhoT0I3CQkJCQkJCQlQaGhoaFI/QTIzNSoXCgsLWmhoaGhoUUVEQ0RKRDEfDWhoXFxoaGhjZWRILzpJRjhoXBwcWGhoaFtbYg4QIS8waFwcKVhoaF8cHF8REREREWhnXFxoaGhfHClOEhISEhJVU2hoaGhoaF9fNBMTExMTVz5MZmhoaFdMTSYUFBQUFDxhVGhoaGhdPi4VFhYWFhZpVmBoaGhoWSgYGhoaGhppgAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAEAACgAAAAgAAAAQAAAAAEACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFL8AABTAAAEWvAABF74AABbCAAAXwwABGMAAABjDAAAYxAAAGsUAABrGAAAbxgAAG8cAAh3BAAEcxQAAHMgAAR3HAAgewgAAHcoAAB/LAAAgzAAFIckAiEYiAAAhzgAAI88AACPQAAAk0AAAJc8AACXSAAUmzQAAJ9IAACfTABAqxwAAKNQAjE0rAAAp1QAAKdYAACrXAAAr2AAGLdMAAC3ZAAAu2wAAL9wAADPVAAAx3QAAMt8AADPgAAA04QAAONQAADnXAAA24wAGN94ACjfdAAA34wAAN+QACznbAAA45QAJOeAAFTzUABY81AAAOeYAFj3UAAo74AAAP9YAADrnAAs74AAAPtoABT/pAABF2wAARd0AAEbaAABI1gAASdoAEETmAABJ2wAAS98AAFHfAAFV3ABTc2EAWYA+AAFa4gAAXd0AAF3hADyeAAABZd4AAWTjADZf5wABZuIAQKETAD2jDwBGZ9gARKEYADhk6wA8pBMAPKUVAK+DbABKbN8ATGziAAF15ABTpioAaKE1AEGtJAABfOkAOrIiAH6hRQABgOkAAYLmAAGC5wACgucAXXrkAAGE5wBEr0AAAYToAAGG6gA9s0AAObkuAD20QQA5ui8AaIHhAEe3OABxgeAAAo3sAFasbABshukAOcA5AGmI7gABleoAbovsAAKZ7QCFtmEAAp7vAD7CagCBl+oAPsNuAAKo8QACqvIAA6zyAAOs9AACr/QAkKfxAGLJlQBfypUAZMqWAAO49QDSuawAc8qaAMPCngADwfgAA8H5AAPE+QAEyPsABMr7AATO/AAE0PwABND9AATR/QAE0v0ACNP9ABvW/QDj1MwAMtr9ADvc/QDK0vQA59rTAOjb1ADp3tcA6+DaAG/l/gB15v4A29/0AO3k3wDu5eAA4eT3AJDr/gDy6+cA5uj3APLs6ADj6PsA8+zpAObq+gDz7uoA8+7rANHt+gD07usA6e35APbx7wDy9PwA5fr/APv7+wD2+/8A7fz/APz8+gDw/P8A/fz7APv8/gD9/f4A+P7/AP/+/gD+/v8A////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyMgAAAAAPYyDb4HHx8fHw05dUwAAAAAAAAAAAAAAyMjIAQEBAQFhkYNyXmV3Y2h6dF1TAgEBAQEBAQEBAQEByAQEBAQEBG2Og3JedXxnWYV0XVMDBAQEBAQEBAQEBAQEBQUFBQURhIyDcl5zfGdbhXRdUwYFBQUFBQUFBQUFBQUICAgICDuijYNyXmV3ZE96dF1TBwgICAgICAgICAgICAoKCgoKYbOMg5K/x8esCg5OWFMJCgoKCgoKCgoKCgoKDAwMDAxtusfHx8fHx3gMDAwLDQwMDAwMDAwMDAwMDAwPDw8PEITHx8fHx8fHFQ8PDw8PDw8PDw8PDw8PDw8PDxISEhIgosfHx8fHx8YSEhISEhISEhISEhISEhISEhISExMTEzuzx8fHx8fHrxMTExMTExMTExMTExMTExMTExMUFBQUYbrHx8fHx8epFBQUboiXnJqGVEcUFBQUFBQUFBcXFxdtx8fHx8fHx7YbbJyYj2tigpObnIAXFxcXFxcXGBgYHYTHx8fHx8e7npybcE0wGBgYP1F+ahgYGBgYGBgaGho6osfHx8fHx6icnIcZGhoaGhoaGhoaGhoaGhoaGhwcHGGzx8fHx8fHp5ycnEgeRjErHBwcHBwcHBwcHBwcISEhbbrHx8fHx8e+oZycnJycnJyViVdEISEhISEhISEhIR+Ex8fHx8fHx8fEraCdnJycnJycnJZxSiEhISEhISMjJ6LHx8fHx8fHx8fHx8fAvcBCVXmUnJyZUiMjIyMjJSU6s8fHx6Wlx8fHx8fHx8fHxyUkJUxpmZuKJSUlJSUmJmG6x8WjIhaqx8fHx8fHtLTHJiYmJkVQZksmJiYmJigobcXHx6QiX6rHx8fHx7AiFrUoKCgoKCgoKCgoKCgoKSmEx8fHx6urx8fHx8fHsiJfeykpKSkpKSkpKSkpKSkqKoTHt8fHx8fHx8fHx8fHubk0KioqKioqKioqKioqKiwsbcemx8fHx8fHx8fHwcfHuCwsLCwsLCwsLCwsLCwsLi43s8eQkJ/Hx8fHx8eukJA5Li4uLi4uLi4uLi4uLi4uLi2Ex8fHx8fHx8fHx8fHfy4uLi4uLi4uLi4uLi4uLi8vLzOzx8fHx8fHx8fHx4svLy8vLy8vLy8vLy8vLy8vMjIyMkGEvMfHx8fHx7FWMjIyMjIyMjIyMjIyMjIyMjI2NjY2MjI+drPHx8d9NTY2NjY2NjY2NjY2NjY2NjY2Njg4NlrHx8fHx8fGSTg4ODg4ODg4ODg4ODg4ODg4ODg4yEBAPGDHx8fCXEBAQEBAQEBAQEBAQEBAQEBAQEBAQMjIyENDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0PIyMAAAAOAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAHAAAADKAAAADAAAABgAAAAAQAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARwQAAE8IAABXEAAAXxQAAGcYAARvHAAAfwAAAHsoAFSucAIdGHgAAIs0Ai0kjAAAlzwAHJc8AID59AAAo0gCPTSgAACnTACQ8lwAYMsgAADDaAAI01AA+WkgAADTeABU4zgAEM+YAl1o3AAg52QAANukAADfqAAA65AA5Y0cAAEDYABc+3QAARtYANEPPAJ1lQgA6eSUAGUbeAAFK2wAVRucAMknWAAJQ2gA8hBkANoQfAB9M5QAXTeYANo0FAABT3gAmUeEAVoMvAEaYAAAAXeIASJIcAABg3gA9WeEAPZ4AADefBACneGMAA2TiAAFn3gAwX+oAU4ZkADqiDwAAauIArH9hAKx9aABWZtYAW4pfAFJp2gAAcOIAX2raAEOkJABJoi4AXKErAEtt4ABGpykANa0bAElu6gA6qiYAaXLbAAZ66ABFricAPq4tAFZ24wBseNoAeaVCALiPdwBtedsANrgeAF955gBBsjIAY6dMAFx86gBYfu0AAIjrAD61RwBCuzMAZILpAAKM6QCipWMAYrZCAL+bhwA3wjQAaIbuADy6UABrifEAAJXtAHCL6wBZtmgATLduAG2O8AACnO8APMFkAIuW4wCltXMAq7J+AIKX6AB9mOsAyamaAImb5AA5xXMAi6DjAACq9ACsuYgAT8iDAAaw8wBBzIIAjqbyAACz9wCVqu0AmK3xAJ+u8AClru4AC8P2AKa48wAAx/wAAMv5ALvQtgAJz/4AAND/ALPB9QDDzscAANP8AMDF7QAQ1PwAwsz3ACna/AC9zfoANtv8AObYzACr4sYA6NrOAMvS8wCu5M8ATuD/AGDi/wCy59MAaOP/ANPn1ABp5vwA5eHjAOzk2wDU3vgAwurYAIHq/ADr6t0Akev/AObm8wDh5vYA9e3kAOTq+gDz8OsAsPH/AOjt/QC38v8A6u//AN327QDx8f4A+PT2AM73/gDz9foA+/jzAPX2/AD8+fQA3fr8AP369QD4+v8A+v37AP/++AD8//4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC/vwAAAAAAAAAAAAAAAAF2obO9vr6+vr6+vr28RwAAAAAAAAAAAAAAAAAAAAAAv7+/AAYAAAAAAAAAAAABAgGZfW18vr6+vr6+vr6pAAAAAAAAAAAAAAAAAAAAAQAGAL8BAQEBQ5Cyvru+vb1QAxiff3FgSWS+vr6+vr5HAggWDgETvb67vr2+vrWQQwEGAQEBAQFyvlUTAAEBAQEAAUukfXFgTzhMYFJKc44+ST9IKwEBAQEBAQEBAQFVvnIBAQEBAUO+IwEBAQEAAQEBAVSdf3FgT0hnZ1lNSHlpWz85LwEBAQEBAQEBAQEBI7tDAQECApBYAgICAgICAgICAnWaf3FgUjhnZ2FPP31pUzhILwICAgICAgICAgICAliQAgICArUCAgICAgICAgICApmaf3FgTzlnZ1lNSHlpUzg5LwICAgICAgICAgICAgK1AgICAr0CAgICAgICAgICS6mXf3FgTzNWZVtPMm5pWz9ILAICAgICAgICAgICAgK7AgICAr4CAgICAgICAgICVLWXf3FcdL6+vr6FBAJEUzg4JQICAgICAgICAgICAgK9AgICArsCAgICAgICAgICdb6xiqa+vr6+vrMpCgICEjU4HwICAgICAgICAgICAgK7AgIDA74DAwMDAwMDAwMDmb6+vr6+vr6+vrAKAwMDAwMSAwMDAwMDAwMDAwMDAwO+AwMDA7sDAwMDAwMDAwMDqb6+vr6+vr6+voUDAwMDAwMDAwMDAwMDAwMDAwMDAwO9AwMFBb4FBQUFBQUFBQVLtb6+vr6+vr6+vkUFBQUFBQUFBQUFBQUFBQUFBQUFBQW+BQUFBbsFBQUFBQUFBQVavr6+vr6+vr6+vikFBQUFBQUFBQUFBQUFBQUFBQUFBQW9BQUHB7sHBwcHBwcHBwd1vr6+vr6+vr6+vgcHBwcHBwcHBwcHBwcHBwcHBwcHBwe+BwcHB70HBwcHBwcHBweZvr6+vr6+vr6+vgcHBwciUXuIj4+PhmsqBwcHBwcHBwe+BwcKCr4MCgoKCgoKCgqrvr6+vr6+vr6+vgoKY4uRj5GRkYyPi4uRiDYKCgoKCgq7CgoKCr4MCgoKCgoKCku1vr6+vr6+vr6+r4aPi4twOycgIjBAcIaLj488CgoKCgq9CgoKCr4KCgoKCgoKClS+vr6+vr6+vr60kYyMiyoKCgoKCgoKCgoVKkYVCgoKCgq9CgoMDLsMDAwMDAwMDHW+vr6+vr6+vrubjI+RYw0KCgoKCgoMCgwMDAwMDAwMCgy+CgwMDL4MDAwMDAwMDJm+vr6+vr6+vr2Tj4+PiBUMDAwMDAwMDAwMDAwMDAwMDAy9DwwMDL4MDAwMDAwMIam+vr6+vr6+vrugi4+Mj4uIi4yIflEgDAwMDAwMDAwMDAy8DAwMDL4MDAwMDAwMS7W+vr6+vr6+vr6+npGLj4yPj4+Pj4+PiF8gDAwMDAwMDAy+DAwPDL0PDwwPDA8PWr6+vr6+vr6+vr6+vq2Tj4+Lj4yPj5GLj4+PgTsPDA8MDwy9DA8PD74PDw8PDw8Pdb6+vr6+vr6+vr6+vr67ua2npZyVjI+LkY+Rj4+INA8PDw++Dw8PD70PDw8PDw8Pmb6+vr6+vr6+vr6+vr69vr6+vr69VCpffoyLjI+Jj4EPDw++Dw8PD70PDw8PDw8Pq76+vqEaGqq+vr6+vr6+vr68vr6+NxEPDw82e5GMiVEPDw++Dw8REbwRERERERFLtb6+vgsQCzq+vr6+vr6+vpYaGqy7ERERERERERERERERERG9EREUEb4RFBEUERRdvr6+vhAQoma+vr6+vr6+vgkaCTq+FBEUERQRFBEUERQRFBG+ERQUFL4UFBQUFBR1vr6+vqpBQra+vr6+vr6+uxAQoWa+FBQUFBQUFBQUFBQUFBS9FBQUFL4UFBQUFBR2vr6+vr6+vr6+vr6+vr6+vqpCQrayFBQUFBQUFBQUFBQUFBS+FBQUFL4UFBQUFBRivr6qvr6+vr6+vr6+vr6+vr6+vr6CFBQUFBQUFBQUFBQUFBS9FBQXFL4UFxQXFBdLvrNXvr6+vr6+vr6+vr6+vr6+vr4UFxQXFBcUFxQXFBcUFxS+FBcUF74XFBcUFxQXvr62JFd3s72+vr6+vr67rLO7mKgXFBcUFxQXFBcUFxQXFBe+FxQXF74XFxcXFxcXrr6+vrq9vr6+vr6+vr6+uCRXljEXFxcXFxcXFxcXFxcXFxe+FxcXF74XFxcXFxcXhL2+vr6+vr6+vr6+vr6+vb69gxcXFxcXFxcXFxcXFxcXFxe+FxcXF74XFxcXFxcXG667vr6+vr6+vr6+vr6+vrupFxcXFxcXFxcXFxcXFxcXFxe+FxcXHr4eFx4XHhceFx68vb6+vr6+vr6+vr69vrUXHhceFx4XHhceFx4XHhceFx6+HhceF74XHhceFx4XHhceeLu9vb6+vr6+vb6zhxceFx4XHhceFx4XHhceFx4XHhe+Fx4eHr4eHh4eHh4eHh4eHhctXZm+vr6+sGIeHh4eHh4eHh4eHh4eHh4eHh4eHh69Hh4eHr0eHh4eHh4eHmJsdoKNo7u+vr61LR4eHh4eHh4eHh4eHh4eHh4eHh4eHh69Hh4eHLUcHhweHB4cG6u8vr6+vr6+voMeHB4cHhweHB4cHhweHB4cHhweHB4cHhy1HB4cHpJqHB4cHhweHCaZvr6+vr2DLhwcHhweHB4cHhweHB4cHhweHB4cHhweHGqSHhwcHF6+PRwcHBwcHlR6hINoTigZGRwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcPb5eHBwcHByAvW8cHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBxvvYAcHBwcHBwcXpS3vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vreUXhwcHBy/HBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHL+/vx0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dv7/AAAAAAAMAAIAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAQAAwAAAAAADAAA=</Image> + <Url type="text/html" method="get" template="https://duckduckgo.com/?q={searchTerms}"/> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/data/engine2/manifest.json b/toolkit/components/search/tests/xpcshell/data/engine2/manifest.json new file mode 100644 index 0000000000..7dd4b15931 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine2/manifest.json @@ -0,0 +1,18 @@ +{ + "name": "A second test engine", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine2@search.mozilla.org" + } + }, + "hidden": true, + "description": "A second test search engine (based on DuckDuckGo)", + "chrome_settings_overrides": { + "search_provider": { + "name": "A second test engine", + "search_url": "https://duckduckgo.com/?q={searchTerms}" + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engineImages.xml b/toolkit/components/search/tests/xpcshell/data/engineImages.xml new file mode 100644 index 0000000000..65b550b31b --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engineImages.xml @@ -0,0 +1,22 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> + <ShortName>IconsTest</ShortName> + <Description>IconsTest. Search by Test.</Description> + <InputEncoding>UTF-8</InputEncoding> + <Image width="16" height="16">data:image/x-icon;base64,ico16</Image> + <Image width="32" height="32">data:image/x-icon;base64,ico32</Image> + <Image width="74" height="74">data:image/png;base64,ico74</Image> + <Url type="application/x-suggestions+json" template="http://api.bing.com/osjson.aspx"> + <Param name="query" value="{searchTerms}"/> + <Param name="form" value="MOZW"/> + </Url> + <Url type="text/html" method="GET" template="http://www.bing.com/search"> + <Param name="q" value="{searchTerms}"/> + <MozParam name="pc" condition="pref" pref="ms-pc"/> + <Param name="form" value="MOZW"/> + </Url> + <SearchForm>http://www.bing.com/search</SearchForm> +</SearchPlugin> diff --git a/toolkit/components/search/tests/xpcshell/data/engineImages/manifest.json b/toolkit/components/search/tests/xpcshell/data/engineImages/manifest.json new file mode 100644 index 0000000000..b5366a6eb6 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engineImages/manifest.json @@ -0,0 +1,37 @@ +{ + "name": "IconsTest", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engineImages@search.mozilla.org" + } + }, + "hidden": true, + "description": "IconsTest. Search by Test.", + "icons": { + "16": "data:image/x-icon;base64,ico16" + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "IconsTest", + "search_url": "https://www.bing.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "form", + "value": "MOZW" + }, + { + "name": "pc", + "condition": "pref", + "pref": "ms-pc" + } + ], + "suggest_url": "https://api.bing.com/osjson.aspxquery={searchTerms}&form=MOZW" + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/engineMaker.sjs b/toolkit/components/search/tests/xpcshell/data/engineMaker.sjs new file mode 100644 index 0000000000..4f97bd7635 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engineMaker.sjs @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Dynamically create an OpenSearch search engine offering search suggestions + * via searchSuggestions.sjs. + * + * The engine is constructed by passing a JSON object with engine details as the query string. + */ + +function handleRequest(request, response) { + let engineData = JSON.parse(unescape(request.queryString).replace("+", " ")); + + if (!engineData.baseURL) { + response.setStatusLine(request.httpVersion, 500, "baseURL required"); + return; + } + + engineData.name = engineData.name || "Generated test engine"; + engineData.description = + engineData.description || "Generated test engine description"; + engineData.method = engineData.method || "GET"; + + response.setStatusLine(request.httpVersion, 200, "OK"); + createOpenSearchEngine(response, engineData); +} + +/** + * Create an OpenSearch engine for the given base URL. + * + * @param {Response} response + * The response object to write the engine to. + * @param {object} engineData + * Information about the search engine to write to the response. + */ +function createOpenSearchEngine(response, engineData) { + let params = ""; + let queryString = ""; + if (engineData.queryString) { + queryString = engineData.queryString.replace("&", "&"); + } else if (engineData.method == "POST") { + params = "<Param name='q' value='{searchTerms}'/>"; + } else { + queryString = "?q={searchTerms}"; + } + let type = "type='application/x-suggestions+json'"; + if (engineData.alternativeJSONType) { + type = "type='application/json' rel='suggestions'"; + } + let image = ""; + if (engineData.image) { + image = `<Image width="16" height="16">${engineData.baseURL}${engineData.image}</Image>`; + } + let updateFile = ""; + if (engineData.updateFile) { + updateFile = `<Url type="application/opensearchdescription+xml" + rel="self" + template="${engineData.baseURL}${engineData.updateFile}" /> + `; + } + + let result = `<?xml version='1.0' encoding='utf-8'?> +<OpenSearchDescription xmlns='http://a9.com/-/spec/opensearch/1.1/'> + <ShortName>${engineData.name}</ShortName> + <Description>${engineData.description}</Description> + <InputEncoding>UTF-8</InputEncoding> + <LongName>${engineData.name}</LongName> + ${image} + <Url ${type} method='${engineData.method}' + template='${engineData.baseURL}searchSuggestions.sjs${queryString}'> + ${params} + </Url> + <Url type='text/html' method='${engineData.method}' + template='${engineData.baseURL}${queryString}'/> + ${updateFile} +</OpenSearchDescription> +`; + response.write(result); +} diff --git a/toolkit/components/search/tests/xpcshell/data/engines-no-order-hint.json b/toolkit/components/search/tests/xpcshell/data/engines-no-order-hint.json new file mode 100644 index 0000000000..3c8befc726 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engines-no-order-hint.json @@ -0,0 +1,189 @@ +{ + "data": [ + { + "webExtension": { + "id": "engine@search.mozilla.org", + "name": "Test search engine", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "contextmenu", + "value": "rcs" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "keyword", + "value": "fflb" + } + ], + "suggest_url": "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}" + }, + "appliesTo": [ + { + "included": { "everywhere": true }, + "default": "yes" + } + ] + }, + { + "webExtension": { + "id": "engine-rel-searchform-purpose@search.mozilla.org", + "name": "engine-rel-searchform-purpose", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "contextmenu", + "value": "rcs" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "keyword", + "value": "fflb" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "searchbar", + "value": "sb" + } + ] + }, + "orderHint": 1000, + "appliesTo": [ + { + "included": { "everywhere": true }, + "excluded": { "locales": { "matches": ["de", "fr"] } }, + "default": "no" + } + ] + }, + { + "webExtension": { + "id": "engine-chromeicon@search.mozilla.org", + "name": "engine-chromeicon", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + }, + "orderHint": 1000, + "appliesTo": [ + { + "included": { "everywhere": true }, + "excluded": { "locales": { "matches": ["de", "fr"] } }, + "default": "no" + }, + { + "included": { "regions": ["ru"] }, + "default": "no" + } + ] + }, + { + "webExtension": { + "id": "engine-resourceicon@search.mozilla.org", + "name": "engine-resourceicon", + "search_url": "https://www.google.com/search", + "search_provider": { + "en": { + "name": "engine-resourceicon", + "search_url": "https://www.google.com/search" + }, + "gd": { + "name": "engine-resourceicon-gd", + "search_url": "https://www.google.com/search" + } + } + }, + "appliesTo": [ + { + "included": { "locales": { "matches": ["en-US", "fr"] } }, + "excluded": { + "regions": ["ru"] + }, + "default": "no" + } + ] + }, + { + "webExtension": { + "id": "engine-reordered@search.mozilla.org", + "name": "Test search engine (Reordered)", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "contextmenu", + "value": "rcs" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "keyword", + "value": "fflb" + } + ], + "suggest_url": "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}" + }, + "appliesTo": [ + { + "included": { "everywhere": true }, + "excluded": { "locales": { "matches": ["de", "fr"] } }, + "default": "no" + } + ] + }, + { + "webExtension": { + "id": "engine-pref@search.mozilla.org", + "name": "engine-pref", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "code", + "condition": "pref", + "pref": "code" + }, + { + "name": "test", + "condition": "pref", + "pref": "test" + } + ] + }, + "appliesTo": [ + { + "included": { "everywhere": true }, + "excluded": { "locales": { "matches": ["de"] } }, + "default": "no" + } + ] + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/data/engines.json b/toolkit/components/search/tests/xpcshell/data/engines.json new file mode 100644 index 0000000000..f27a4844df --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engines.json @@ -0,0 +1,203 @@ +{ + "data": [ + { + "webExtension": { + "id": "engine@search.mozilla.org", + "name": "Test search engine", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "contextmenu", + "value": "rcs" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "keyword", + "value": "fflb" + } + ], + "suggest_url": "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}" + }, + "orderHint": 10000, + "appliesTo": [ + { + "included": { "everywhere": true }, + "excluded": { "locales": { "matches": ["gd"] } }, + "default": "yes" + } + ] + }, + { + "webExtension": { + "id": "engine-pref@search.mozilla.org", + "name": "engine-pref", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "code", + "condition": "pref", + "pref": "code" + }, + { + "name": "test", + "condition": "pref", + "pref": "test" + } + ] + }, + "orderHint": 7000, + "appliesTo": [ + { + "included": { "everywhere": true }, + "excluded": { "locales": { "matches": ["de"] } }, + "default": "no", + "defaultPrivate": "yes" + } + ] + }, + { + "webExtension": { + "id": "engine-rel-searchform-purpose@search.mozilla.org", + "name": "engine-rel-searchform-purpose", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "contextmenu", + "value": "rcs" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "keyword", + "value": "fflb" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "searchbar", + "value": "sb" + } + ] + }, + "orderHint": 6000, + "appliesTo": [ + { + "included": { "everywhere": true }, + "excluded": { "locales": { "matches": ["de", "fr"] } }, + "default": "no" + }, + { + "included": { "locales": { "matches": ["gd"] } }, + "orderHint": 9000 + } + ] + }, + { + "webExtension": { + "id": "engine-chromeicon@search.mozilla.org", + "name": "engine-chromeicon", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + }, + "orderHint": 8000, + "appliesTo": [ + { + "included": { "everywhere": true }, + "excluded": { "locales": { "matches": ["de", "fr"] } }, + "default": "no" + }, + { + "included": { "regions": ["ru"] }, + "default": "no" + } + ] + }, + { + "webExtension": { + "id": "engine-resourceicon@search.mozilla.org", + "default_locale": "en", + "searchProvider": { + "en": { + "name": "engine-resourceicon", + "search_url": "https://www.google.com/search" + }, + "gd": { + "name": "engine-resourceicon-gd", + "search_url": "https://www.google.com/search" + } + } + }, + "orderHint": 9000, + "appliesTo": [ + { + "included": { "locales": { "matches": ["en-US", "fr"] } }, + "excluded": { "regions": ["ru"] }, + "default": "no" + }, + { + "included": { "locales": { "matches": ["gd"] } }, + "default": "yes", + "webExtension": { + "locales": ["gd"] + } + } + ] + }, + { + "webExtension": { + "id": "engine-reordered@search.mozilla.org", + "name": "Test search engine (Reordered)", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "contextmenu", + "value": "rcs" + }, + { + "name": "channel", + "condition": "purpose", + "purpose": "keyword", + "value": "fflb" + } + ], + "suggest_url": "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}" + }, + "orderHint": 5000, + "appliesTo": [ + { + "included": { "everywhere": true }, + "excluded": { "locales": { "matches": ["de", "fr"] } }, + "default": "no" + } + ] + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/_locales/af/messages.json b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/_locales/af/messages.json new file mode 100644 index 0000000000..29ddd24df5 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/_locales/af/messages.json @@ -0,0 +1,17 @@ +{ + "extensionName": { + "message": "Multilocale" + }, + "extensionDescription": { + "message": "Wikipedia, die vrye ensiklopedie" + }, + "url_lang": { + "message": "af" + }, + "searchUrl": { + "message": "https://af.wikipedia.org/wiki/Spesiaal:Soek" + }, + "suggestUrl": { + "message": "https://af.wikipedia.org/w/api.php" + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/_locales/an/messages.json b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/_locales/an/messages.json new file mode 100644 index 0000000000..d21d910463 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/_locales/an/messages.json @@ -0,0 +1,17 @@ +{ + "extensionName": { + "message": "Multilocale" + }, + "extensionDescription": { + "message": "A enciclopedia Libre" + }, + "url_lang": { + "message": "an" + }, + "searchUrl": { + "message": "https://an.wikipedia.org/wiki/Especial:Mirar" + }, + "suggestUrl": { + "message": "https://an.wikipedia.org/w/api.php" + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/favicon.ico b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/favicon.ico Binary files differnew file mode 100644 index 0000000000..4314071e24 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/favicon.ico diff --git a/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/manifest.json b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/manifest.json new file mode 100644 index 0000000000..0fd835ca40 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/geolookup-extensions/multilocale/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "__MSG_extensionName__", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "multilocale@search.mozilla.org" + } + }, + "hidden": true, + "description": "__MSG_extensionDescription__", + "icons": { + "16": "favicon.ico" + }, + "default_locale": "af", + "chrome_settings_overrides": { + "search_provider": { + "name": "__MSG_extensionName__", + "search_url": "__MSG_searchUrl__", + "suggest_url": "__MSG_searchUrl__" + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/iconsRedirect.sjs b/toolkit/components/search/tests/xpcshell/data/iconsRedirect.sjs new file mode 100644 index 0000000000..98f9aed4d0 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/iconsRedirect.sjs @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Redirect a request for an icon to a different place, using a different + * content-type. + */ + +function handleRequest(request, response) { + response.setStatusLine("1.1", 302, "Moved"); + if (request.queryString == "type=invalid") { + response.setHeader("Content-Type", "image/png", false); + response.setHeader("Location", "/head_search.js", false); + } else { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Location", "remoteIcon.ico", false); + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/remoteIcon.ico b/toolkit/components/search/tests/xpcshell/data/remoteIcon.ico Binary files differnew file mode 100644 index 0000000000..442ab4dc80 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/remoteIcon.ico diff --git a/toolkit/components/search/tests/xpcshell/data/search-legacy-correct-default-engine-hashes.json b/toolkit/components/search/tests/xpcshell/data/search-legacy-correct-default-engine-hashes.json new file mode 100644 index 0000000000..e6091b7230 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/search-legacy-correct-default-engine-hashes.json @@ -0,0 +1,112 @@ +{ + "version": 1, + "buildID": "20121106", + "locale": "en-US", + "metaData": { + "current": "engine2", + "private": "engine2" + }, + "engines": [ + { + "_name": "engine1", + "_shortName": "engine1", + "_loadPath": "[other]addEngineWithDetails:engine1@search.mozilla.org", + "description": "A small test engine", + "__searchForm": null, + "_iconURL": "moz-extension://9c38b851-bede-2244-a086-9be8128dd64d/favicon.ico", + "_iconMapObj": { + "{}": "moz-extension://9c38b851-bede-2244-a086-9be8128dd64d/favicon.ico" + }, + "_metaData": { + "alias": "testAlias" + }, + "_urls": [ + { + "template": "https://1.example.com/search", + "rels": [], + "resultDomain": "1.example.com", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + ], + "_isBuiltin": true, + "queryCharset": "UTF-8", + "extensionID": "engine1@search.mozilla.org" + }, + { + "_name": "engine2", + "_shortName": "engine2", + "_loadPath": "[other]addEngineWithDetails:engine2@search.mozilla.org", + "description": "A small test engine", + "__searchForm": null, + "_iconURL": "moz-extension://0ea1d9b5-a14c-0e42-afaf-f25e8261c135/favicon.ico", + "_iconMapObj": { + "{}": "moz-extension://0ea1d9b5-a14c-0e42-afaf-f25e8261c135/favicon.ico" + }, + "_metaData": { + "alias": null, + "hidden": false + }, + "_urls": [ + { + "template": "https://2.example.com/search", + "rels": [], + "resultDomain": "2.example.com", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + ], + "_isBuiltin": true, + "queryCharset": "UTF-8", + "extensionID": "engine2@search.mozilla.org" + }, + { + "_name": "Test search engine", + "_shortName": "test-search-engine", + "description": "A test search engine (based on Google search)", + "__searchForm": "http://www.google.com/", + "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA", + "_metaData": {}, + "_urls": [ + { + "template": "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}", + "rels": [], + "resultDomain": "suggestqueries.google.com", + "type": "application/x-suggestions+json", + "params": [] + }, + { + "template": "http://www.google.com/search", + "rels": [], + "resultDomain": "google.com", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "channel", + "value": "fflb", + "purpose": "keyword" + }, + { + "name": "channel", + "value": "rcs", + "purpose": "contextmenu" + } + ] + } + ], + "queryCharset": "UTF-8", + "extensionID": "test-addon-id@mozilla.org" + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/data/search-legacy-no-ids.json b/toolkit/components/search/tests/xpcshell/data/search-legacy-no-ids.json new file mode 100644 index 0000000000..733c323876 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/search-legacy-no-ids.json @@ -0,0 +1,87 @@ +{ + "version": 6, + "engines": [ + { "_name": "Google", "_isAppProvided": true, "_metaData": { "order": 1 } }, + { + "_name": "Wikipedia (en)", + "_isAppProvided": true, + "_metaData": { "order": 7 } + }, + { "_name": "Bing", "_isAppProvided": true, "_metaData": { "order": 3 } }, + { + "_name": "Amazon.co.uk", + "_isAppProvided": true, + "_metaData": { "order": 2 } + }, + { + "_name": "DuckDuckGo", + "_isAppProvided": true, + "_metaData": { "order": 4 } + }, + { "_name": "eBay", "_isAppProvided": true, "_metaData": { "order": 5 } }, + { + "_name": "Policy", + "_loadPath": "[other]addEngineWithDetails:set-via-policy", + "_metaData": { "alias": "PolicyAlias", "order": 6 } + }, + { + "_name": "Bugzilla@Mozilla", + "_loadPath": "[https]bugzilla.mozilla.org/bugzillamozilla.xml", + "description": "Bugzilla@Mozilla Quick Search", + "_metaData": { + "loadPathHash": "Bxz6jVe3IIBxLLaafUus536LMyLKoGZm7xsBv/yiTw8=", + "order": 8, + "alias": "bugzillaAlias" + }, + "_urls": [ + { + "params": [], + "rels": [], + "template": "https://bugzilla.mozilla.org/buglist.cgi?quicksearch={searchTerms}" + } + ], + "_orderHint": null, + "_telemetryId": null, + "_updateInterval": null, + "_updateURL": null, + "_iconUpdateURL": null, + "_extensionID": null, + "_locale": null, + "_definedAliases": [] + }, + { + "_name": "User", + "_loadPath": "[other]addEngineWithDetails:set-via-user", + "_metaData": { + "order": 9, + "alias": "UserAlias" + }, + "_urls": [ + { + "params": [], + "rels": [], + "template": "https://example.com/test?q={searchTerms}" + } + ], + "_telemetryId": null, + "_updateInterval": null, + "_updateURL": null, + "_iconUpdateURL": null, + "_extensionID": null, + "_locale": null, + "_hasPreferredIcon": null + }, + { "_name": "Amazon.com", "_isAppProvided": true, "_metaData": {} } + ], + "metaData": { + "useSavedOrder": true, + "locale": "en-US", + "region": "GB", + "channel": "default", + "experiment": "", + "distroID": "", + "appDefaultEngine": "Google", + "current": "Bing", + "hash": "5Of6s1D+BDjPRti1wqtFyBTH1PnOf9n6cRwWlEXZhd0=" + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/search-legacy-old-loadPaths.json b/toolkit/components/search/tests/xpcshell/data/search-legacy-old-loadPaths.json new file mode 100644 index 0000000000..f716fb3e4a --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/search-legacy-old-loadPaths.json @@ -0,0 +1,135 @@ +{ + "version": 7, + "engines": [ + { + "id": "google@search.mozilla.orgdefault", + "_name": "Google", + "_isAppProvided": true, + "_metaData": { "order": 1 } + }, + { + "id": "amazon@search.mozilla.orgen-GB", + "_name": "Amazon.co.uk", + "_isAppProvided": true, + "_metaData": { "order": 2 } + }, + { + "id": "bing@search.mozilla.orgdefault", + "_name": "Bing", + "_isAppProvided": true, + "_metaData": { "order": 3 } + }, + { + "id": "ddg@search.mozilla.orgdefault", + "_name": "DuckDuckGo", + "_isAppProvided": true, + "_metaData": { "order": 4 } + }, + { + "id": "ebay@search.mozilla.orguk", + "_name": "eBay", + "_isAppProvided": true, + "_metaData": { "order": 5 } + }, + { + "id": "wikipedia@search.mozilla.orgdefault", + "_name": "Wikipedia (en)", + "_isAppProvided": true, + "_metaData": { "order": 6 } + }, + { + "id": "policy-Policy", + "_name": "Policy", + "_loadPath": "[other]addEngineWithDetails:set-via-policy", + "_metaData": { "alias": "PolicyAlias", "order": 6 } + }, + { + "id": "bbc163e7-7b1a-47aa-a32c-c59062de2753", + "_name": "Bugzilla@Mozilla", + "_loadPath": "[https]bugzilla.mozilla.org/bugzillamozilla.xml", + "description": "Bugzilla@Mozilla Quick Search", + "_metaData": { + "loadPathHash": "Bxz6jVe3IIBxLLaafUus536LMyLKoGZm7xsBv/yiTw8=", + "order": 8, + "alias": "bugzillaAlias" + }, + "_urls": [ + { + "params": [], + "rels": [], + "template": "https://bugzilla.mozilla.org/buglist.cgi?quicksearch={searchTerms}" + } + ], + "_orderHint": null, + "_telemetryId": null, + "_updateInterval": null, + "_updateURL": null, + "_iconUpdateURL": null, + "_extensionID": null, + "_locale": null, + "_definedAliases": [] + }, + { + "id": "bbc163e7-7b1a-47aa-a32c-c59062de2754", + "_name": "User", + "_loadPath": "[other]addEngineWithDetails:set-via-user", + "_metaData": { + "order": 9, + "alias": "UserAlias" + }, + "_urls": [ + { + "params": [], + "rels": [], + "template": "https://example.com/test?q={searchTerms}" + } + ], + "_telemetryId": null, + "_updateInterval": null, + "_updateURL": null, + "_iconUpdateURL": null, + "_extensionID": null, + "_locale": null, + "_hasPreferredIcon": null + }, + { + "id": "example@tests.mozilla.orgdefault", + "_name": "Example", + "_loadPath": "[other]addEngineWithDetails:example@tests.mozilla.org", + "description": null, + "_iconURL": "", + "_metaData": {}, + "_urls": [ + { + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ], + "rels": [], + "template": "https://example.com/" + } + ], + "_telemetryId": null, + "_updateInterval": null, + "_updateURL": null, + "_iconUpdateURL": null, + "_extensionID": "example@tests.mozilla.org", + "_locale": "default", + "_definedAliases": [], + "_hasPreferredIcon": null + } + ], + "metaData": { + "useSavedOrder": true, + "locale": "en-US", + "region": "GB", + "channel": "default", + "experiment": "", + "distroID": "", + "appDefaultEngine": "Google", + "current": "Bing", + "hash": "5Of6s1D+BDjPRti1wqtFyBTH1PnOf9n6cRwWlEXZhd0=" + } +} diff --git a/toolkit/components/search/tests/xpcshell/data/search-legacy-wrong-default-engine-hashes.json b/toolkit/components/search/tests/xpcshell/data/search-legacy-wrong-default-engine-hashes.json new file mode 100644 index 0000000000..ca7081f565 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/search-legacy-wrong-default-engine-hashes.json @@ -0,0 +1,114 @@ +{ + "version": 1, + "buildID": "20121106", + "locale": "en-US", + "metaData": { + "current": "engine2", + "private": "engine2", + "hash": "wrong-hash-o/HzjHlVpb97AFGH3pY1GZ6CoTQkQslUKRd38/qasto=", + "privateHash": "wrong-hash-o/HzjHlVpb97AFGH3pY1GZ6CoTQkQslUKRd38/qasto=" + }, + "engines": [ + { + "_name": "engine1", + "_shortName": "engine1", + "_loadPath": "[other]addEngineWithDetails:engine1@search.mozilla.org", + "description": "A small test engine", + "__searchForm": null, + "_iconURL": "moz-extension://9c38b851-bede-2244-a086-9be8128dd64d/favicon.ico", + "_iconMapObj": { + "{}": "moz-extension://9c38b851-bede-2244-a086-9be8128dd64d/favicon.ico" + }, + "_metaData": { + "alias": "testAlias" + }, + "_urls": [ + { + "template": "https://1.example.com/search", + "rels": [], + "resultDomain": "1.example.com", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + ], + "_isBuiltin": true, + "queryCharset": "UTF-8", + "extensionID": "engine1@search.mozilla.org" + }, + { + "_name": "engine2", + "_shortName": "engine2", + "_loadPath": "[other]addEngineWithDetails:engine2@search.mozilla.org", + "description": "A small test engine", + "__searchForm": null, + "_iconURL": "moz-extension://0ea1d9b5-a14c-0e42-afaf-f25e8261c135/favicon.ico", + "_iconMapObj": { + "{}": "moz-extension://0ea1d9b5-a14c-0e42-afaf-f25e8261c135/favicon.ico" + }, + "_metaData": { + "alias": null, + "hidden": false + }, + "_urls": [ + { + "template": "https://2.example.com/search", + "rels": [], + "resultDomain": "2.example.com", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + ], + "_isBuiltin": true, + "queryCharset": "UTF-8", + "extensionID": "engine2@search.mozilla.org" + }, + { + "_name": "Test search engine", + "_shortName": "test-search-engine", + "description": "A test search engine (based on Google search)", + "__searchForm": "http://www.google.com/", + "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA", + "_metaData": {}, + "_urls": [ + { + "template": "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}", + "rels": [], + "resultDomain": "suggestqueries.google.com", + "type": "application/x-suggestions+json", + "params": [] + }, + { + "template": "http://www.google.com/search", + "rels": [], + "resultDomain": "google.com", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "channel", + "value": "fflb", + "purpose": "keyword" + }, + { + "name": "channel", + "value": "rcs", + "purpose": "contextmenu" + } + ] + } + ], + "queryCharset": "UTF-8", + "extensionID": "test-addon-id@mozilla.org" + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/data/search-legacy-wrong-third-party-engine-hashes.json b/toolkit/components/search/tests/xpcshell/data/search-legacy-wrong-third-party-engine-hashes.json new file mode 100644 index 0000000000..25329e083f --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/search-legacy-wrong-third-party-engine-hashes.json @@ -0,0 +1,114 @@ +{ + "version": 1, + "buildID": "20121106", + "locale": "en-US", + "metaData": { + "current": "Test search engine", + "private": "Test search engine", + "hash": "wrong-hash-o/HzjHlVpb97AFGH3pY1GZ6CoTQkQslUKRd38/qasto=", + "privateHash": "wrong-hash-o/HzjHlVpb97AFGH3pY1GZ6CoTQkQslUKRd38/qasto=" + }, + "engines": [ + { + "_name": "engine1", + "_shortName": "engine1", + "_loadPath": "[other]addEngineWithDetails:engine1@search.mozilla.org", + "description": "A small test engine", + "__searchForm": null, + "_iconURL": "moz-extension://9c38b851-bede-2244-a086-9be8128dd64d/favicon.ico", + "_iconMapObj": { + "{}": "moz-extension://9c38b851-bede-2244-a086-9be8128dd64d/favicon.ico" + }, + "_metaData": { + "alias": "testAlias" + }, + "_urls": [ + { + "template": "https://1.example.com/search", + "rels": [], + "resultDomain": "1.example.com", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + ], + "_isBuiltin": true, + "queryCharset": "UTF-8", + "extensionID": "engine1@search.mozilla.org" + }, + { + "_name": "engine2", + "_shortName": "engine2", + "_loadPath": "[other]addEngineWithDetails:engine2@search.mozilla.org", + "description": "A small test engine", + "__searchForm": null, + "_iconURL": "moz-extension://0ea1d9b5-a14c-0e42-afaf-f25e8261c135/favicon.ico", + "_iconMapObj": { + "{}": "moz-extension://0ea1d9b5-a14c-0e42-afaf-f25e8261c135/favicon.ico" + }, + "_metaData": { + "alias": null, + "hidden": false + }, + "_urls": [ + { + "template": "https://2.example.com/search", + "rels": [], + "resultDomain": "2.example.com", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + ], + "_isBuiltin": true, + "queryCharset": "UTF-8", + "extensionID": "engine2@search.mozilla.org" + }, + { + "_name": "Test search engine", + "_shortName": "test-search-engine", + "description": "A test search engine (based on Google search)", + "__searchForm": "http://www.google.com/", + "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA", + "_metaData": {}, + "_urls": [ + { + "template": "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}", + "rels": [], + "resultDomain": "suggestqueries.google.com", + "type": "application/x-suggestions+json", + "params": [] + }, + { + "template": "http://www.google.com/search", + "rels": [], + "resultDomain": "google.com", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "channel", + "value": "fflb", + "purpose": "keyword" + }, + { + "name": "channel", + "value": "rcs", + "purpose": "contextmenu" + } + ] + } + ], + "queryCharset": "UTF-8", + "extensionID": "test-addon-id@mozilla.org" + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/data/search-legacy.json b/toolkit/components/search/tests/xpcshell/data/search-legacy.json new file mode 100644 index 0000000000..c8416f3813 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/search-legacy.json @@ -0,0 +1,109 @@ +{ + "version": 1, + "buildID": "20121106", + "locale": "en-US", + "metaData": {}, + "engines": [ + { + "_name": "engine1", + "_shortName": "engine1", + "_loadPath": "[other]addEngineWithDetails:engine1@search.mozilla.org", + "description": "A small test engine", + "__searchForm": null, + "_iconURL": "moz-extension://9c38b851-bede-2244-a086-9be8128dd64d/favicon.ico", + "_iconMapObj": { + "{}": "moz-extension://9c38b851-bede-2244-a086-9be8128dd64d/favicon.ico" + }, + "_metaData": { + "alias": "testAlias" + }, + "_urls": [ + { + "template": "https://1.example.com/search", + "rels": [], + "resultDomain": "1.example.com", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + ], + "_isBuiltin": true, + "queryCharset": "UTF-8", + "extensionID": "engine1@search.mozilla.org" + }, + { + "_name": "engine2", + "_shortName": "engine2", + "_loadPath": "[other]addEngineWithDetails:engine2@search.mozilla.org", + "description": "A small test engine", + "__searchForm": null, + "_iconURL": "moz-extension://0ea1d9b5-a14c-0e42-afaf-f25e8261c135/favicon.ico", + "_iconMapObj": { + "{}": "moz-extension://0ea1d9b5-a14c-0e42-afaf-f25e8261c135/favicon.ico" + }, + "_metaData": { + "alias": null, + "hidden": true + }, + "_urls": [ + { + "template": "https://2.example.com/search", + "rels": [], + "resultDomain": "2.example.com", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + ], + "_isBuiltin": true, + "queryCharset": "UTF-8", + "extensionID": "engine2@search.mozilla.org" + }, + { + "_name": "Test search engine", + "_shortName": "test-search-engine", + "description": "A test search engine (based on Google search)", + "__searchForm": "http://www.google.com/", + "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA", + "_metaData": {}, + "_urls": [ + { + "template": "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}", + "rels": [], + "resultDomain": "suggestqueries.google.com", + "type": "application/x-suggestions+json", + "params": [] + }, + { + "template": "http://www.google.com/search", + "rels": [], + "resultDomain": "google.com", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "channel", + "value": "fflb", + "purpose": "keyword" + }, + { + "name": "channel", + "value": "rcs", + "purpose": "contextmenu" + } + ] + } + ], + "queryCharset": "UTF-8", + "extensionID": "test-addon-id@mozilla.org" + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/data/search-migration.json b/toolkit/components/search/tests/xpcshell/data/search-migration.json new file mode 100644 index 0000000000..520149a370 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/search-migration.json @@ -0,0 +1,68 @@ +{ + "version": 1, + "buildID": "20121106", + "locale": "en-US", + "metaData": {}, + "engines": [ + { + "_name": "engine1", + "_metaData": { + "alias": "testAlias" + }, + "_isAppProvided": true + }, + { + "_name": "engine2", + "_metaData": { + "alias": null, + "hidden": true + }, + "_isAppProvided": true + }, + { + "_name": "simple", + "_loadPath": "jar:[profile]/extensions/simple@tests.mozilla.org.xpi!/simple.xml", + "_shortName": "simple", + "description": "A migration test engine", + "__searchForm": "http://www.example.com/", + "_metaData": {}, + "_urls": [ + { + "template": "http://www.example.com/search", + "rels": [], + "resultDomain": "google.com", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + ], + "queryCharset": "UTF-8" + }, + { + "_name": "simple search", + "_loadPath": "[other]addEngineWithDetails:simple@tests.mozilla.org", + "_shortName": "simple search", + "description": "A migration test engine", + "__searchForm": "http://www.example.com/", + "_metaData": {}, + "_urls": [ + { + "template": "http://www.example.com/search", + "rels": [], + "resultDomain": "google.com", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + ], + "queryCharset": "UTF-8", + "_extensionID": "simple@tests.mozilla.org" + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/data/search-obsolete-app.json b/toolkit/components/search/tests/xpcshell/data/search-obsolete-app.json new file mode 100644 index 0000000000..151359ff73 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/search-obsolete-app.json @@ -0,0 +1,41 @@ +{ + "version": 1, + "buildID": "20121106", + "locale": "en-US", + "metaData": {}, + "engines": [ + { + "_name": "engine1", + "_metaData": { + "alias": "testAlias" + }, + "_isAppProvided": true + }, + { + "_name": "engine2", + "_metaData": { + "alias": null, + "hidden": true + }, + "_isAppProvided": true + }, + { + "_name": "App", + "_shortName": "app", + "_loadPath": "jar:[app]/omni.ja!distribution.xml", + "description": "App Search", + "__searchForm": null, + "_metaData": {}, + "_urls": [ + { + "template": "https://example.com/search", + "rels": ["searchform"], + "resultDomain": "example.com", + "params": [] + } + ], + "queryCharset": "UTF-8", + "_readOnly": false + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/data/search-obsolete-distribution.json b/toolkit/components/search/tests/xpcshell/data/search-obsolete-distribution.json new file mode 100644 index 0000000000..efc609a5af --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/search-obsolete-distribution.json @@ -0,0 +1,41 @@ +{ + "version": 1, + "buildID": "20121106", + "locale": "en-US", + "metaData": {}, + "engines": [ + { + "_name": "engine1", + "_metaData": { + "alias": "testAlias" + }, + "_isAppProvided": true + }, + { + "_name": "engine2", + "_metaData": { + "alias": null, + "hidden": true + }, + "_isAppProvided": true + }, + { + "_name": "Distribution", + "_shortName": "distribution", + "_loadPath": "[distribution]/searchplugins/common/distribution.xml", + "description": "Distribution Search", + "__searchForm": null, + "_metaData": {}, + "_urls": [ + { + "template": "https://example.com/search", + "rels": ["searchform"], + "resultDomain": "example.com", + "params": [] + } + ], + "queryCharset": "UTF-8", + "_readOnly": false + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/data/search-obsolete-langpack.json b/toolkit/components/search/tests/xpcshell/data/search-obsolete-langpack.json new file mode 100644 index 0000000000..8c45b4d61f --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/search-obsolete-langpack.json @@ -0,0 +1,91 @@ +{ + "version": 1, + "buildID": "20121106", + "locale": "en-US", + "metaData": {}, + "engines": [ + { + "_name": "engine1", + "_metaData": { + "alias": "testAlias" + }, + "_isAppProvided": true + }, + { + "_name": "engine2", + "_metaData": { + "alias": null, + "hidden": true + }, + "_isAppProvided": true + }, + { + "_name": "Langpack", + "_shortName": "langpack-ru", + "_loadPath": "jar:[app]/extensions/langpack-ru@firefox.mozilla.org.xpi!browser/langpack.xml", + "description": "Langpack search", + "__searchForm": null, + "_metaData": {}, + "_urls": [ + { + "template": "https://example.com/search", + "rels": ["searchform"], + "resultDomain": "example.com", + "params": [] + } + ], + "queryCharset": "UTF-8" + }, + { + "_name": "Langpack1", + "_shortName": "langpack1-ru", + "_loadPath": "[app]/extensions/langpack-ru@firefox.mozilla.org.xpi!browser/langpack1.xml", + "description": "Langpack1 search", + "__searchForm": null, + "_metaData": {}, + "_urls": [ + { + "template": "https://example1.com/search", + "rels": ["searchform"], + "resultDomain": "example1.com", + "params": [] + } + ], + "queryCharset": "UTF-8" + }, + { + "_name": "Langpack2", + "_shortName": "langpack2-ru", + "_loadPath": "jar:[profile]/extensions/langpack-ru@firefox.mozilla.org.xpi!browser/langpack2.xml", + "description": "Langpack2 search", + "__searchForm": null, + "_metaData": {}, + "_urls": [ + { + "template": "https://example2.com/search", + "rels": ["searchform"], + "resultDomain": "example2.com", + "params": [] + } + ], + "queryCharset": "UTF-8" + }, + { + "_name": "Langpack3", + "_shortName": "langpack3-ru", + "_loadPath": "jar:[other]/langpack-ru@firefox.mozilla.org.xpi!browser/langpack3.xml", + "description": "Langpack3 search", + "__searchForm": null, + "_metaData": {}, + "_urls": [ + { + "template": "https://example3.com/search", + "rels": ["searchform"], + "resultDomain": "example3.com", + "params": [] + } + ], + "queryCharset": "UTF-8" + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/data/search.json b/toolkit/components/search/tests/xpcshell/data/search.json new file mode 100644 index 0000000000..f757af2ab3 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/search.json @@ -0,0 +1,66 @@ +{ + "version": 1, + "buildID": "20121106", + "locale": "en-US", + "metaData": {}, + "engines": [ + { + "id": "engine1@search.mozilla.orgdefault", + "_name": "engine1", + "_metaData": { + "alias": "testAlias" + }, + "_isAppProvided": true + }, + { + "id": "engine2@search.mozilla.orgdefault", + "_name": "engine2", + "_metaData": { + "alias": null, + "hidden": true + }, + "_isAppProvided": true + }, + { + "id": "test-addon-id@mozilla.orgdefault", + "_name": "Test search engine", + "_shortName": "test-search-engine", + "description": "A test search engine (based on Google search)", + "__searchForm": "http://www.google.com/", + "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA", + "_metaData": {}, + "_urls": [ + { + "template": "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}", + "rels": [], + "resultDomain": "suggestqueries.google.com", + "type": "application/x-suggestions+json", + "params": [] + }, + { + "template": "http://www.google.com/search", + "rels": [], + "resultDomain": "google.com", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "channel", + "value": "fflb", + "purpose": "searchbar" + }, + { + "name": "channel", + "value": "rcs", + "purpose": "contextmenu" + } + ] + } + ], + "queryCharset": "UTF-8", + "_extensionID": "test-addon-id@mozilla.org" + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/data/searchSuggestions.sjs b/toolkit/components/search/tests/xpcshell/data/searchSuggestions.sjs new file mode 100644 index 0000000000..2eaa2684c4 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/searchSuggestions.sjs @@ -0,0 +1,187 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +let { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +let { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); + +/** + * Provide search suggestions in the OpenSearch JSON format. + */ + +function handleRequest(request, response) { + // Get the query parameters from the query string. + let query = parseQueryString(request.queryString); + + function convertToUtf8(str) { + return String.fromCharCode(...new TextEncoder().encode(str)); + } + + function writeSuggestions(q, completions = []) { + let jsonString = JSON.stringify([q, completions]); + + // This script must be evaluated as UTF-8 for this to write out the bytes of + // the string in UTF-8. If it's evaluated as Latin-1, the written bytes + // will be the result of UTF-8-encoding the result-string *twice*, which + // will break the "I ❤️" case further down. + let stringOfUtf8Bytes = convertToUtf8(jsonString); + + response.write(stringOfUtf8Bytes); + } + + /** + * Sends `data` as suggestions directly. This is useful when testing rich + * suggestions, which do not conform to the object shape sent by + * writeSuggestions. + * + * @param {Array} data The data to send as suggestions. + */ + function writeSuggestionsDirectly(data) { + let jsonString = JSON.stringify(data); + let stringOfUtf8Bytes = convertToUtf8(jsonString); + response.setHeader("Content-Type", "application/json", false); + response.write(stringOfUtf8Bytes); + } + + response.setStatusLine(request.httpVersion, 200, "OK"); + + let q = request.method == "GET" ? query.q : undefined; + if (q == "cookie") { + response.setHeader("Set-Cookie", "cookie=1"); + writeSuggestions(q); + } else if (q == "no remote" || q == "no results") { + writeSuggestions(q); + } else if (q == "Query Mismatch") { + writeSuggestions("This is an incorrect query string", ["some result"]); + } else if (q == "Query Case Mismatch") { + writeSuggestions(q.toUpperCase(), [q]); + } else if (q == "") { + writeSuggestions("", ["The server should never be sent an empty query"]); + } else if (q?.startsWith("mo")) { + writeSuggestions(q, ["Mozilla", "modern", "mom"]); + } else if (q?.startsWith("I ❤️")) { + writeSuggestions(q, ["I ❤️ Mozilla"]); + } else if (q?.startsWith("stü")) { + writeSuggestions("st\\u00FC", ["stühle", "stüssy"]); + } else if (q?.startsWith("tailjunk ")) { + writeSuggestionsDirectly([ + q, + [q + " normal", q + " tail 1", q + " tail 2"], + [], + { + "google:irrelevantparameter": [], + "google:badformat": { + "google:suggestdetail": [ + {}, + { mp: "… ", t: "tail 1" }, + { mp: "… ", t: "tail 2" }, + ], + }, + }, + ]); + } else if (q?.startsWith("tailjunk few ")) { + writeSuggestionsDirectly([ + q, + [q + " normal", q + " tail 1", q + " tail 2"], + [], + { + "google:irrelevantparameter": [], + "google:badformat": { + "google:suggestdetail": [{ mp: "… ", t: "tail 1" }], + }, + }, + ]); + } else if (q?.startsWith("tailalt ")) { + writeSuggestionsDirectly([ + q, + [q + " normal", q + " tail 1", q + " tail 2"], + { + "google:suggestdetail": [ + {}, + { mp: "… ", t: "tail 1" }, + { mp: "… ", t: "tail 2" }, + ], + }, + ]); + } else if (q?.startsWith("tail ")) { + writeSuggestionsDirectly([ + q, + [q + " normal", q + " tail 1", q + " tail 2"], + [], + { + "google:irrelevantparameter": [], + "google:suggestdetail": [ + {}, + { mp: "… ", t: "tail 1" }, + { mp: "… ", t: "tail 2" }, + ], + }, + ]); + } else if (q?.startsWith("richempty ")) { + writeSuggestionsDirectly([ + q, + [q + " normal", q + " tail 1", q + " tail 2"], + [], + { + "google:irrelevantparameter": [], + "google:suggestdetail": [], + }, + ]); + } else if (q?.startsWith("letter ")) { + let letters = []; + for ( + let charCode = "A".charCodeAt(); + charCode <= "Z".charCodeAt(); + charCode++ + ) { + letters.push("letter " + String.fromCharCode(charCode)); + } + writeSuggestions(q, letters); + } else if (q?.startsWith("HTTP ")) { + response.setStatusLine(request.httpVersion, q.replace("HTTP ", ""), q); + writeSuggestions(q, [q]); + } else if (q == "invalidJSON") { + response.setHeader("Content-Type", "application/json", false); + response.write('["invalid"]'); + } else if (q == "invalidContentType") { + response.setHeader("Content-Type", "text/xml", false); + writeSuggestions(q, ["invalidContentType response"]); + } else if (q?.startsWith("delay")) { + // Delay the response by delayMs milliseconds. 200ms is the default, less + // than the timeout but hopefully enough to abort before completion. + let match = /^delay([0-9]+)/.exec(q); + let delayMs = match ? parseInt(match[1]) : 200; + response.processAsync(); + writeSuggestions(q, [q]); + setTimeout(() => response.finish(), delayMs); + } else if (q?.startsWith("slow ")) { + // Delay the response by 10 seconds so the client timeout is reached. + response.processAsync(); + writeSuggestions(q, [q]); + setTimeout(() => response.finish(), 10000); + } else if (request.method == "POST") { + // This includes headers, not just the body + let requestText = NetUtil.readInputStreamToString( + request.bodyInputStream, + request.bodyInputStream.available() + ); + // Only use the last line which contains the encoded params + let requestLines = requestText.split("\n"); + let postParams = parseQueryString(requestLines[requestLines.length - 1]); + writeSuggestions(postParams.q, ["Mozilla", "modern", "mom"]); + } else { + response.setStatusLine(request.httpVersion, 404, "Not Found"); + } +} + +function parseQueryString(queryString) { + let query = {}; + queryString.split("&").forEach(function (val) { + let [name, value] = val.split("="); + query[name] = decodeURIComponent(value).replace(/[+]/g, " "); + }); + return query; +} diff --git a/toolkit/components/search/tests/xpcshell/data/search_ignorelist.json b/toolkit/components/search/tests/xpcshell/data/search_ignorelist.json new file mode 100644 index 0000000000..35240893ec --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/search_ignorelist.json @@ -0,0 +1,51 @@ +{ + "version": 1, + "buildID": "20121106", + "locale": "en-US", + "metaData": {}, + "engines": [ + { + "_name": "Test search engine", + "_shortName": "test-search-engine", + "description": "A test search engine (based on Google search)", + "extensionID": "test-addon-id@mozilla.org", + "__searchForm": "http://www.google.com/", + "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA", + "_metaData": {}, + "_urls": [ + { + "template": "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}", + "rels": [], + "type": "application/x-suggestions+json", + "params": [] + }, + { + "template": "http://www.google.com/search", + "resultDomain": "google.com", + "rels": [], + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "ignore", + "value": "true" + }, + { + "name": "channel", + "value": "fflb", + "purpose": "keyword" + }, + { + "name": "channel", + "value": "rcs", + "purpose": "contextmenu" + } + ] + } + ], + "queryCharset": "UTF-8" + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/data/svgIcon.svg b/toolkit/components/search/tests/xpcshell/data/svgIcon.svg new file mode 100644 index 0000000000..e2550f8d5d --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/svgIcon.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" + width="16" height="16" viewBox="0 0 16 16"> + <rect x="4" y="4" width="8px" height="8px" style="fill: blue" /> +</svg> diff --git a/toolkit/components/search/tests/xpcshell/data1/engine1/manifest.json b/toolkit/components/search/tests/xpcshell/data1/engine1/manifest.json new file mode 100644 index 0000000000..5fa44ea692 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data1/engine1/manifest.json @@ -0,0 +1,27 @@ +{ + "name": "engine1", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine1@search.mozilla.org" + } + }, + "hidden": true, + "description": "A small test engine", + "icons": { + "16": "favicon.ico" + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "engine1", + "search_url": "https://1.example.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data1/engine2/manifest.json b/toolkit/components/search/tests/xpcshell/data1/engine2/manifest.json new file mode 100644 index 0000000000..7ab094198b --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data1/engine2/manifest.json @@ -0,0 +1,27 @@ +{ + "name": "engine2", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine2@search.mozilla.org" + } + }, + "hidden": true, + "description": "A small test engine", + "icons": { + "16": "favicon.ico" + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "engine2", + "search_url": "https://2.example.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data1/engines.json b/toolkit/components/search/tests/xpcshell/data1/engines.json new file mode 100644 index 0000000000..92ffbd48a0 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data1/engines.json @@ -0,0 +1,92 @@ +{ + "data": [ + { + "webExtension": { + "id": "engine1@search.mozilla.org", + "name": "engine1", + "search_url": "https://1.example.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + }, + "orderHint": 10000, + "appliesTo": [ + { + "included": { "everywhere": true }, + "default": "yes-if-no-other", + "defaultPrivate": "yes-if-no-other" + } + ] + }, + { + "webExtension": { + "id": "engine2@search.mozilla.org", + "name": "engine2", + "search_url": "https://2.example.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + }, + "orderHint": 7000, + "appliesTo": [ + { + "included": { "everywhere": true }, + "default": "no" + }, + { + "included": { "everywhere": true }, + "default": "yes", + "experiment": "exp1" + } + ] + }, + { + "webExtension": { + "id": "exp2@search.mozilla.org", + "name": "exp2", + "search_url": "https://2.example.com/searchexp", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + }, + "orderHint": 5000, + "appliesTo": [ + { + "included": { "everywhere": true }, + "defaultPrivate": "yes", + "experiment": "exp2" + } + ] + }, + { + "webExtension": { + "id": "exp3@search.mozilla.org", + "name": "exp3", + "search_url": "https://3.example.com/searchexp", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + }, + "orderHint": 20000, + "appliesTo": [ + { + "included": { "everywhere": true }, + "default": "yes", + "experiment": "exp3" + } + ] + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/data1/exp2/manifest.json b/toolkit/components/search/tests/xpcshell/data1/exp2/manifest.json new file mode 100644 index 0000000000..0cd0e080b9 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data1/exp2/manifest.json @@ -0,0 +1,27 @@ +{ + "name": "exp2", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "exp2@search.mozilla.org" + } + }, + "hidden": true, + "description": "A small test engine", + "icons": { + "16": "favicon.ico" + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "exp2", + "search_url": "https://2.example.com/searchexp", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data1/exp3/manifest.json b/toolkit/components/search/tests/xpcshell/data1/exp3/manifest.json new file mode 100644 index 0000000000..4e023e0fef --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data1/exp3/manifest.json @@ -0,0 +1,27 @@ +{ + "name": "exp3", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "exp3@search.mozilla.org" + } + }, + "hidden": true, + "description": "A small test engine", + "icons": { + "16": "favicon.ico" + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "exp3", + "search_url": "https://3.example.com/searchexp", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/data1/search-config-v2.json b/toolkit/components/search/tests/xpcshell/data1/search-config-v2.json new file mode 100644 index 0000000000..98bdfa26ff --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data1/search-config-v2.json @@ -0,0 +1,101 @@ +{ + "data": [ + { + "base": { + "name": "engine1", + "urls": { + "search": { + "base": "https://1.example.com/search", + "searchTermParamName": "q" + } + }, + "classification": "unknown" + }, + "variants": [{ "environment": { "allRegionsAndLocales": true } }], + "identifier": "engine1", + "recordType": "engine" + }, + { + "base": { + "name": "engine2", + "urls": { + "search": { + "base": "https://2.example.com/search", + "searchTermParamName": "q" + } + }, + "classification": "unknown" + }, + "variants": [{ "environment": { "allRegionsAndLocales": true } }], + "identifier": "engine2", + "recordType": "engine" + }, + { + "base": { + "name": "exp2", + "urls": { + "search": { + "base": "https://2.example.com/searchexp", + "searchTermParamName": "q" + } + }, + "classification": "unknown" + }, + "variants": [ + { + "environment": { "allRegionsAndLocales": true, "experiment": "exp2" } + } + ], + "identifier": "exp2", + "recordType": "engine" + }, + { + "base": { + "name": "exp3", + "urls": { + "search": { + "base": "https://3.example.com/searchexp", + "searchTermParamName": "q" + } + }, + "classification": "unknown" + }, + "variants": [ + { + "environment": { "allRegionsAndLocales": true, "experiment": "exp3" } + } + ], + "identifier": "exp3", + "recordType": "engine" + }, + { + "recordType": "defaultEngines", + "globalDefault": "engine1", + "globalDefaultPrivate": "engine1", + "specificDefaults": [ + { + "environment": { "experiment": "exp1" }, + "default": "engine2" + }, + { + "environment": { "experiment": "exp2" }, + "defaultPrivate": "exp2" + }, + { + "environment": { "experiment": "exp3" }, + "default": "exp3" + } + ] + }, + { + "recordType": "engineOrders", + "orders": [ + { + "environment": { "allRegionsAndLocales": true }, + "order": ["exp3", "engine1", "engine2", "exp2"] + } + ] + } + ], + "timestamp": 1704229342821 +} diff --git a/toolkit/components/search/tests/xpcshell/head_search.js b/toolkit/components/search/tests/xpcshell/head_search.js new file mode 100644 index 0000000000..1c0504e277 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/head_search.js @@ -0,0 +1,508 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + Region: "resource://gre/modules/Region.sys.mjs", + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + RemoteSettingsClient: + "resource://services-settings/RemoteSettingsClient.sys.mjs", + SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs", + SearchService: "resource://gre/modules/SearchService.sys.mjs", + SearchSettings: "resource://gre/modules/SearchSettings.sys.mjs", + SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs", + SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", + clearTimeout: "resource://gre/modules/Timer.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +var { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); +var { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +SearchTestUtils.init(this); + +const SETTINGS_FILENAME = "search.json.mozlz4"; + +// nsSearchService.js uses Services.appinfo.name to build a salt for a hash. +// eslint-disable-next-line mozilla/use-services +var XULRuntime = Cc["@mozilla.org/xre/runtime;1"].getService(Ci.nsIXULRuntime); + +// Expand the amount of information available in error logs +Services.prefs.setBoolPref("browser.search.log", true); +Services.prefs.setBoolPref("browser.region.log", true); + +AddonTestUtils.init(this, false); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +// Allow telemetry probes which may otherwise be disabled for some applications (e.g. Thunderbird) +Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true +); + +// For tests, allow the settings to write sooner than it would do normally so that +// the tests that need to wait for it can run a bit faster. +SearchSettings.SETTNGS_INVALIDATION_DELAY = 250; + +async function promiseSettingsData() { + let path = PathUtils.join(PathUtils.profileDir, SETTINGS_FILENAME); + return IOUtils.readJSON(path, { decompress: true }); +} + +function promiseSaveSettingsData(data) { + return IOUtils.writeJSON( + PathUtils.join(PathUtils.profileDir, SETTINGS_FILENAME), + data, + { compress: true } + ); +} + +async function promiseEngineMetadata() { + let settings = await promiseSettingsData(); + let data = {}; + for (let engine of settings.engines) { + data[engine._name] = engine._metaData; + } + return data; +} + +async function promiseGlobalMetadata() { + return (await promiseSettingsData()).metaData; +} + +async function promiseSaveGlobalMetadata(globalData) { + let data = await promiseSettingsData(); + data.metaData = globalData; + await promiseSaveSettingsData(data); +} + +function promiseDefaultNotification(type = "normal") { + return SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE[ + type == "private" ? "DEFAULT_PRIVATE" : "DEFAULT" + ], + SearchUtils.TOPIC_ENGINE_MODIFIED + ); +} + +/** + * Clean the profile of any settings file left from a previous run. + * + * @returns {boolean} + * Indicates if the settings file existed. + */ +function removeSettingsFile() { + let file = do_get_profile().clone(); + file.append(SETTINGS_FILENAME); + if (file.exists()) { + file.remove(false); + return true; + } + return false; +} + +/** + * isUSTimezone taken from nsSearchService.js + * + * @returns {boolean} + */ +function isUSTimezone() { + // Timezone assumptions! We assume that if the system clock's timezone is + // between Newfoundland and Hawaii, that the user is in North America. + + // This includes all of South America as well, but we have relatively few + // en-US users there, so that's OK. + + // 150 minutes = 2.5 hours (UTC-2.5), which is + // Newfoundland Daylight Time (http://www.timeanddate.com/time/zones/ndt) + + // 600 minutes = 10 hours (UTC-10), which is + // Hawaii-Aleutian Standard Time (http://www.timeanddate.com/time/zones/hast) + + let UTCOffset = new Date().getTimezoneOffset(); + return UTCOffset >= 150 && UTCOffset <= 600; +} + +const kTestEngineName = "Test search engine"; + +/** + * Waits for the settings file to be saved. + * + * @returns {Promise} Resolved when the settings file is saved. + */ +function promiseAfterSettings() { + return SearchTestUtils.promiseSearchNotification( + "write-settings-to-disk-complete" + ); +} + +/** + * Sets the home region, and waits for the search service to reload the engines. + * + * @param {string} region + * The region to set. + */ +async function promiseSetHomeRegion(region) { + let promise = SearchTestUtils.promiseSearchNotification("engines-reloaded"); + Region._setHomeRegion(region); + await promise; +} + +/** + * Sets the requested/available locales and waits for the search service to + * reload the engines. + * + * @param {string} locale + * The locale to set. + */ +async function promiseSetLocale(locale) { + if (!Services.locale.availableLocales.includes(locale)) { + throw new Error( + `"${locale}" needs to be included in Services.locales.availableLocales at the start of the test.` + ); + } + + let promise = SearchTestUtils.promiseSearchNotification("engines-reloaded"); + Services.locale.requestedLocales = [locale]; + await promise; +} + +/** + * Read a JSON file and return the JS object + * + * @param {nsIFile} file + * The file to read. + * @returns {object} + * Returns the JSON object if the file was successfully read, + * false otherwise. + */ +async function readJSONFile(file) { + return JSON.parse(await IOUtils.readUTF8(file.path)); +} + +/** + * Recursively compare two objects and check that every property of expectedObj has the same value + * on actualObj. + * + * @param {object} expectedObj + * The source object that we expect to match + * @param {object} actualObj + * The object to check against the source + * @param {Function} skipProp + * A function that is called with the property name and its value, to see if + * testing that property should be skipped or not. + */ +function isSubObjectOf(expectedObj, actualObj, skipProp) { + for (let prop in expectedObj) { + if (skipProp && skipProp(prop, expectedObj[prop])) { + continue; + } + if (expectedObj[prop] instanceof Object) { + Assert.equal( + actualObj[prop]?.length, + expectedObj[prop].length, + `Should have the correct length for property ${prop}` + ); + isSubObjectOf(expectedObj[prop], actualObj[prop], skipProp); + } else { + Assert.equal( + actualObj[prop], + expectedObj[prop], + `Should have the correct value for property ${prop}` + ); + } + } +} + +/** + * After useHttpServer() is called, this string contains the URL of the "data" + * directory, including the final slash. + */ +var gDataUrl; + +/** + * Initializes the HTTP server and ensures that it is terminated when tests end. + * + * @param {string} dir + * The test sub-directory to use for the engines. + * @returns {HttpServer} + * The HttpServer object in case further customization is needed. + */ +function useHttpServer(dir = "data") { + let httpServer = new HttpServer(); + httpServer.start(-1); + httpServer.registerDirectory("/", do_get_cwd()); + gDataUrl = `http://localhost:${httpServer.identity.primaryPort}/${dir}/`; + registerCleanupFunction(async function cleanup_httpServer() { + await new Promise(resolve => { + httpServer.stop(resolve); + }); + }); + return httpServer; +} + +// This "enum" from nsSearchService.js +const TELEMETRY_RESULT_ENUM = { + SUCCESS: 0, + SUCCESS_WITHOUT_DATA: 1, + TIMEOUT: 2, + ERROR: 3, +}; + +/** + * Checks the value of the SEARCH_SERVICE_COUNTRY_FETCH_RESULT probe. + * + * @param {string|null} aExpectedValue + * If a value from TELEMETRY_RESULT_ENUM, we expect to see this value + * recorded exactly once in the probe. If |null|, we expect to see + * nothing recorded in the probe at all. + */ +function checkCountryResultTelemetry(aExpectedValue) { + let histogram = Services.telemetry.getHistogramById( + "SEARCH_SERVICE_COUNTRY_FETCH_RESULT" + ); + let snapshot = histogram.snapshot(); + if (aExpectedValue != null) { + equal(snapshot.values[aExpectedValue], 1); + } else { + deepEqual(snapshot.values, {}); + } +} + +/** + * Provides a basic set of remote settings for use in tests. + */ +async function setupRemoteSettings() { + const settings = await RemoteSettings("hijack-blocklists"); + sinon.stub(settings, "get").returns([ + { + id: "load-paths", + matches: ["[addon]searchignore@mozilla.com"], + _status: "synced", + }, + { + id: "submission-urls", + matches: ["ignore=true"], + _status: "synced", + }, + ]); +} + +/** + * Helper function that sets up a server and respnds to region + * fetch requests. + * + * @param {string} region + * The region that the server will respond with. + * @param {Promise|null} waitToRespond + * A promise that the server will await on to delay responding + * to the request. + */ +function useCustomGeoServer(region, waitToRespond = Promise.resolve()) { + let srv = useHttpServer(); + srv.registerPathHandler("/fetch_region", async (req, res) => { + res.processAsync(); + await waitToRespond; + res.setStatusLine("1.1", 200, "OK"); + res.write(JSON.stringify({ country_code: region })); + res.finish(); + }); + + Services.prefs.setCharPref( + "browser.region.network.url", + `http://localhost:${srv.identity.primaryPort}/fetch_region` + ); +} + +/** + * @typedef {object} TelemetryDetails + * @property {string} engineId + * The telemetry ID for the search engine. + * @property {string} [displayName] + * The search engine's display name. + * @property {string} [loadPath] + * The load path for the search engine. + * @property {string} [submissionUrl] + * The submission URL for the search engine. + * @property {string} [verified] + * Whether the search engine is verified. + */ + +/** + * Asserts that default search engine telemetry has been correctly reported + * to Glean. + * + * @param {object} expected + * An object containing telemetry details for normal and private engines. + * @param {TelemetryDetails} expected.normal + * An object with the expected details for the normal search engine. + * @param {TelemetryDetails} [expected.private] + * An object with the expected details for the private search engine. + */ +async function assertGleanDefaultEngine(expected) { + await TestUtils.waitForCondition( + () => + Glean.searchEngineDefault.engineId.testGetValue() == + (expected.normal.engineId ?? ""), + "Should have set the correct telemetry id for the normal engine" + ); + + await TestUtils.waitForCondition( + () => + Glean.searchEnginePrivate.engineId.testGetValue() == + (expected.private?.engineId ?? ""), + "Should have set the correct telemetry id for the private engine" + ); + + for (let property of [ + "displayName", + "loadPath", + "submissionUrl", + "verified", + ]) { + if (property in expected.normal) { + Assert.equal( + Glean.searchEngineDefault[property].testGetValue(), + expected.normal[property] ?? "", + `Should have set ${property} correctly` + ); + } + if (expected.private && property in expected.private) { + Assert.equal( + Glean.searchEnginePrivate[property].testGetValue(), + expected.private[property] ?? "", + `Should have set ${property} correctly` + ); + } + } +} + +/** + * A simple observer to ensure we get only the expected notifications. + */ +class SearchObserver { + constructor(expectedNotifications, returnEngineForNotification = false) { + this.observer = this.observer.bind(this); + this.deferred = Promise.withResolvers(); + this.expectedNotifications = expectedNotifications; + this.returnEngineForNotification = returnEngineForNotification; + + Services.obs.addObserver(this.observer, SearchUtils.TOPIC_ENGINE_MODIFIED); + + this.timeout = setTimeout(this.handleTimeout.bind(this), 5000); + } + + get promise() { + return this.deferred.promise; + } + + handleTimeout() { + this.deferred.reject( + new Error( + "Waiting for Notifications timed out, only received: " + + this.expectedNotifications.join(",") + ) + ); + } + + observer(subject, topic, data) { + Assert.greater( + this.expectedNotifications.length, + 0, + "Should be expecting a notification" + ); + Assert.equal( + data, + this.expectedNotifications[0], + "Should have received the next expected notification" + ); + + if ( + this.returnEngineForNotification && + data == this.returnEngineForNotification + ) { + this.engineToReturn = subject.QueryInterface(Ci.nsISearchEngine); + } + + this.expectedNotifications.shift(); + + if (!this.expectedNotifications.length) { + clearTimeout(this.timeout); + delete this.timeout; + this.deferred.resolve(this.engineToReturn); + Services.obs.removeObserver( + this.observer, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + } + } +} + +/** + * Some tests might trigger initialisation which will trigger the search settings + * update. We need to make sure we wait for that to finish before we exit, otherwise + * it may cause shutdown issues. + */ +let updatePromise = SearchTestUtils.promiseSearchNotification( + "settings-update-complete" +); + +registerCleanupFunction(async () => { + if (Services.search.isInitialized) { + await updatePromise; + } +}); + +let consoleAllowList = [ + // Harness issues. + 'property "localProfileDir" is non-configurable and can\'t be deleted', + 'property "profileDir" is non-configurable and can\'t be deleted', +]; + +let endConsoleListening = TestUtils.listenForConsoleMessages(); + +registerCleanupFunction(async () => { + let msgs = await endConsoleListening(); + for (let msg of msgs) { + msg = msg.wrappedJSObject; + if (msg.level != "error") { + continue; + } + + if (!msg.arguments?.length) { + Assert.ok( + false, + "Unexpected console message received during test: " + msg + ); + } else { + let firstArg = msg.arguments[0]; + // Use the appropriate message depending on the object supplied to + // the first argument. + let message = firstArg.messageContents ?? firstArg.message ?? firstArg; + if (!consoleAllowList.some(e => message.includes(e))) { + Assert.ok( + false, + "Unexpected console message received during test: " + message + ); + } + } + } +}); diff --git a/toolkit/components/search/tests/xpcshell/method-extensions/engines.json b/toolkit/components/search/tests/xpcshell/method-extensions/engines.json new file mode 100644 index 0000000000..ddd4e45acc --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/method-extensions/engines.json @@ -0,0 +1,55 @@ +{ + "data": [ + { + "webExtension": { + "id": "get@search.mozilla.org", + "name": "Get Engine", + "search_url": "https://example.com", + "search_url_get_params": "webExtension=1&search={searchTerms}", + "suggest_url": "https://example.com", + "suggest_url_get_params": "webExtension=1&suggest={searchTerms}" + }, + "params": { + "searchUrlGetParams": [ + { "name": "config", "value": "1" }, + { "name": "search", "value": "{searchTerms}" } + ], + "suggestUrlGetParams": [ + { "name": "config", "value": "1" }, + { "name": "suggest", "value": "{searchTerms}" } + ] + }, + "appliesTo": [ + { + "included": { "everywhere": true }, + "default": "yes" + } + ] + }, + { + "webExtension": { + "id": "post@search.mozilla.org", + "name": "Post Engine", + "search_url": "https://example.com", + "search_url_post_params": "webExtension=1&search={searchTerms}", + "suggest_url": "https://example.com", + "suggest_url_post_params": "webExtension=1&suggest={searchTerms}" + }, + "params": { + "searchUrlPostParams": [ + { "name": "config", "value": "1" }, + { "name": "search", "value": "{searchTerms}" } + ], + "suggestUrlPostParams": [ + { "name": "config", "value": "1" }, + { "name": "suggest", "value": "{searchTerms}" } + ] + }, + "appliesTo": [ + { + "included": { "everywhere": true } + } + ] + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/method-extensions/get/manifest.json b/toolkit/components/search/tests/xpcshell/method-extensions/get/manifest.json new file mode 100644 index 0000000000..a85cdaaa0f --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/method-extensions/get/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "Get Engine", + "manifest_version": 2, + "version": "1.0", + "description": "Get engine to test get params", + "browser_specific_settings": { + "gecko": { + "id": "get@search.mozilla.org" + } + }, + "hidden": true, + "chrome_settings_overrides": { + "search_provider": { + "name": "Get Engine", + "search_url": "https://example.com", + "search_url_get_params": "webExtension=1&search={searchTerms}", + "suggest_url": "https://example.com", + "suggest_url_get_params": "webExtension=1&suggest={searchTerms}" + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/method-extensions/post/manifest.json b/toolkit/components/search/tests/xpcshell/method-extensions/post/manifest.json new file mode 100644 index 0000000000..dce9bfb512 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/method-extensions/post/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "Post Engine", + "manifest_version": 2, + "version": "1.0", + "description": "Get engine to test ost params", + "browser_specific_settings": { + "gecko": { + "id": "post@search.mozilla.org" + } + }, + "hidden": true, + "chrome_settings_overrides": { + "search_provider": { + "name": "Post Engine", + "search_url": "https://example.com", + "search_url_post_params": "webExtension=1&search={searchTerms}", + "suggest_url": "https://example.com", + "suggest_url_post_params": "webExtension=1&suggest={searchTerms}" + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/opensearch/chromeicon.xml b/toolkit/components/search/tests/xpcshell/opensearch/chromeicon.xml new file mode 100644 index 0000000000..856732c6d6 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/chromeicon.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>engine-chromeicon</ShortName> +<Image width="16" height="16">chrome://branding/content/icon16.png</Image> +<Image width="32" height="32">chrome://branding/content/icon32.png</Image> +<Url type="text/html" method="GET" template="http://www.google.com/search"> + <Param name="q" value="{searchTerms}"/> +</Url> +</SearchPlugin> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-insecurely-updated1.xml b/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-insecurely-updated1.xml new file mode 100644 index 0000000000..3131c25f37 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-insecurely-updated1.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/"> +<ShortName>ii1</ShortName> +<Description>Insecure and insecurely updated 1</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="http://example.com/ii1"> + <Param name="q" value="{searchTerms}"/> +</Url> +<Url type="application/opensearchdescription+xml" + rel="self" + template="http://example.com/ii1.xml" /> +<SearchForm>http://example.com/ii1</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-insecurely-updated2.xml b/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-insecurely-updated2.xml new file mode 100644 index 0000000000..a3c850d4d9 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-insecurely-updated2.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/"> +<ShortName>ii2</ShortName> +<Description>Insecure and insecurely updated 2</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="http://example.com/ii2"> + <Param name="q" value="{searchTerms}"/> +</Url> +<Url type="application/opensearchdescription+xml" + rel="self" + template="http://example.com/ii2.xml" /> +<SearchForm>http://example.com/ii2</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-no-update-url1.xml b/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-no-update-url1.xml new file mode 100644 index 0000000000..75a5da8e7f --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-no-update-url1.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/"> +<ShortName>inu1</ShortName> +<Description>Insecure and no update URL 1</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="http://example.com/inu1"> + <Param name="q" value="{searchTerms}"/> +</Url> +<SearchForm>http://example.com/inu1</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-securely-updated1.xml b/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-securely-updated1.xml new file mode 100644 index 0000000000..9427747722 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/insecure-and-securely-updated1.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/"> +<ShortName>is1</ShortName> +<Description>Insecure and securely updated 1</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="http://example.com/is1"> + <Param name="q" value="{searchTerms}"/> +</Url> +<Url type="application/opensearchdescription+xml" + rel="self" + template="https://example.com/is1.xml" /> +<SearchForm>http://example.com/is1</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/invalid.xml b/toolkit/components/search/tests/xpcshell/opensearch/invalid.xml new file mode 100644 index 0000000000..e8efce6726 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/invalid.xml @@ -0,0 +1 @@ +# An invalid xml engine file. diff --git a/toolkit/components/search/tests/xpcshell/opensearch/mozilla-ns.xml b/toolkit/components/search/tests/xpcshell/opensearch/mozilla-ns.xml new file mode 100644 index 0000000000..f185f94868 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/mozilla-ns.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>mozilla-ns</ShortName> +<Description>An engine using mozilla namespace</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="https://example.com/search"> + <Param name="q" value="{searchTerms}"/> + <MozParam name="channel" condition="purpose" purpose="searchbar" value="test"/> +</Url> +<SearchForm>https://example.com/</SearchForm> +</SearchPlugin> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/post.xml b/toolkit/components/search/tests/xpcshell/opensearch/post.xml new file mode 100644 index 0000000000..621e49c872 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/post.xml @@ -0,0 +1,8 @@ +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>Post</ShortName> + <Url type="text/html" method="POST" template="https://example.com/post"> + <Param name="searchterms" value="{searchTerms}"/> + </Url> + <Url type="text/html" method="POST" template="http://engine-rel-searchform-post.xml/POST" rel="searchform"/> + <SearchForm>http://engine-rel-searchform-post.xml/?search</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/resourceicon.xml b/toolkit/components/search/tests/xpcshell/opensearch/resourceicon.xml new file mode 100644 index 0000000000..32861c34ea --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/resourceicon.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>engine-resourceicon</ShortName> +<Image width="16" height="16">resource://search-extensions/icon16.png</Image> +<Image width="32" height="32">resource://search-extensions/icon32.png</Image> +<Url type="text/html" method="GET" template="http://www.google.com/search"> + <Param name="q" value="{searchTerms}"/> +</Url> +</SearchPlugin> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/searchform-invalid.xml b/toolkit/components/search/tests/xpcshell/opensearch/searchform-invalid.xml new file mode 100644 index 0000000000..4cc059b59e --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/searchform-invalid.xml @@ -0,0 +1,10 @@ +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" + xmlns:moz="http://www.mozilla.org/2006/browser/search/"> + <ShortName>searchform-invalid</ShortName> + <Description>Bug 483086 Test 1</Description> + <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image> + <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/?search"> + <Param name="test" value="{searchTerms}"/> + </Url> + <moz:SearchForm>foo://example.com</moz:SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-and-insecurely-updated1.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-insecurely-updated1.xml new file mode 100644 index 0000000000..d8a62d0e18 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-insecurely-updated1.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/"> +<ShortName>si1</ShortName> +<Description>Secure and insecurely updated 1</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="https://example.com/si1"> + <Param name="q" value="{searchTerms}"/> +</Url> +<Url type="application/opensearchdescription+xml" + rel="self" + template="http://example.com/si1.xml" /> +<SearchForm>https://example.com/si1</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-and-insecurely-updated2.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-insecurely-updated2.xml new file mode 100644 index 0000000000..f707e5eb3d --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-insecurely-updated2.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/"> +<ShortName>si2</ShortName> +<Description>Secure and insecurely updated 2</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="https://example.com/si2"> + <Param name="q" value="{searchTerms}"/> +</Url> +<Url type="application/opensearchdescription+xml" + rel="self" + template="http://example.com/si2.xml" /> +<SearchForm>https://example.com/si2</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-and-no-update-url1.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-no-update-url1.xml new file mode 100644 index 0000000000..6dcbbb126c --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-no-update-url1.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/"> +<ShortName>snu1</ShortName> +<Description>Secure and no update URL 1</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="https://example.com/snu1"> + <Param name="q" value="{searchTerms}"/> +</Url> +<SearchForm>https://example.com/snu1</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated-insecure-form.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated-insecure-form.xml new file mode 100644 index 0000000000..15a0b6a517 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated-insecure-form.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/"> +<ShortName>ssif</ShortName> +<Description>Secure and securely updated insecure form</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="https://example.com/ssif"> + <Param name="q" value="{searchTerms}"/> +</Url> +<Url type="application/opensearchdescription+xml" + rel="self" + template="https://example.com/ssif.xml" /> +<SearchForm>http://example.com/ssif</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated1.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated1.xml new file mode 100644 index 0000000000..593c8bec8c --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated1.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/"> +<ShortName>ss1</ShortName> +<Description>Secure and securely updated 1</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="https://example.com/ss1"> + <Param name="q" value="{searchTerms}"/> +</Url> +<Url type="application/opensearchdescription+xml" + rel="self" + template="https://example.com/ss1.xml" /> +<SearchForm>https://example.com/ss1</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated2.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated2.xml new file mode 100644 index 0000000000..30a20b754a --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated2.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/"> +<ShortName>ss2</ShortName> +<Description>Secure and securely updated 2</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="https://example.com/ss2"> + <Param name="q" value="{searchTerms}"/> +</Url> +<Url type="application/opensearchdescription+xml" + rel="self" + template="https://example.com/ss2.xml" /> +<SearchForm>https://example.com/ss2</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated3.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated3.xml new file mode 100644 index 0000000000..8b86a82199 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-and-securely-updated3.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/"> +<ShortName>ss3</ShortName> +<Description>Secure and securely updated 3</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="https://example.com/ss3"> + <Param name="q" value="{searchTerms}"/> +</Url> +<Url type="application/opensearchdescription+xml" + rel="self" + template="https://example.com/ss3.xml" /> +<SearchForm>https://example.com/ss3</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-localhost.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-localhost.xml new file mode 100644 index 0000000000..89d96f2c43 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-localhost.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/"> +<ShortName>sl</ShortName> +<Description>Secure localhost</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="http://localhost:8080"> + <Param name="q" value="{searchTerms}"/> +</Url> +<SearchForm>http://localhost:8080/sl</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-onionv2.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-onionv2.xml new file mode 100644 index 0000000000..8da3995a71 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-onionv2.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/"> +<ShortName>sov2</ShortName> +<Description>Secure onion v2</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="http://s3zkf3ortukqklec.onion"> + <Param name="q" value="{searchTerms}"/> +</Url> +<SearchForm>http://s3zkf3ortukqklec.onion/sov2</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/secure-onionv3.xml b/toolkit/components/search/tests/xpcshell/opensearch/secure-onionv3.xml new file mode 100644 index 0000000000..c8256ca28a --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/secure-onionv3.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/"> +<ShortName>sov3</ShortName> +<Description>Secure onion v3</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="http://ydemw5wg5cseltau22u4fjfrmfshopaldpoznsirb3rgo2gv6uh4s2y5.onion"> + <Param name="q" value="{searchTerms}"/> +</Url> +<SearchForm>http://ydemw5wg5cseltau22u4fjfrmfshopaldpoznsirb3rgo2gv6uh4s2y5.onion/sov3</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/simple.xml b/toolkit/components/search/tests/xpcshell/opensearch/simple.xml new file mode 100644 index 0000000000..ee38e51bca --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/simple.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.0/"> +<ShortName>simple</ShortName> +<Description>A small test engine</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="https://example.com/search"> + <Param name="q" value="{searchTerms}"/> +</Url> +<SearchForm>https://example.com/</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/suggestion-alternate.xml b/toolkit/components/search/tests/xpcshell/opensearch/suggestion-alternate.xml new file mode 100644 index 0000000000..7a961520b9 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/suggestion-alternate.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearchdescription/1.1/"> +<ShortName>suggestion-alternate</ShortName> +<Description>A small engine with suggestions</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="https://example.com/search"> + <Param name="q" value="{searchTerms}"/> +</Url> +<Url type="application/json" rel="suggestions" method="GET" + template="https://example.com/suggest"> + <Param name="suggestion" value="{searchTerms}"/> +</Url> + +<SearchForm>https://example.com/</SearchForm> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/opensearch/suggestion.xml b/toolkit/components/search/tests/xpcshell/opensearch/suggestion.xml new file mode 100644 index 0000000000..8d2f701a36 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/opensearch/suggestion.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearchdescription/1.0/"> +<ShortName>suggestion</ShortName> +<Description>A small engine with suggestions</Description> +<InputEncoding>windows-1252</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC</Image> +<Url type="text/html" method="GET" template="https://example.com/search"> + <Param name="q" value="{searchTerms}"/> +</Url> +<Url type="application/x-suggestions+json" method="GET" + template="https://example.com/suggest"> + <Param name="suggestion" value="{searchTerms}"/> +</Url> +<Url type="text/html" method="GET" template="http://engine-rel-searchform.xml/?search" rel="searchform"/> +</OpenSearchDescription> diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js b/toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js new file mode 100644 index 0000000000..72c4d4f04f --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js @@ -0,0 +1,604 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + // Only needed when SearchUtils.newSearchConfigEnabled is false. + AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs", + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", + Region: "resource://gre/modules/Region.sys.mjs", + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + SearchEngine: "resource://gre/modules/SearchEngine.sys.mjs", + SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs", + SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs", + SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", + SearchEngineSelectorOld: + "resource://gre/modules/SearchEngineSelectorOld.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", + updateAppInfo: "resource://testing-common/AppInfo.sys.mjs", +}); + +const GLOBAL_SCOPE = this; +const TEST_DEBUG = Services.env.get("TEST_DEBUG"); + +const URLTYPE_SUGGEST_JSON = "application/x-suggestions+json"; +const URLTYPE_SEARCH_HTML = "text/html"; +const SUBMISSION_PURPOSES = [ + "searchbar", + "keyword", + "contextmenu", + "homepage", + "newtab", +]; + +let engineSelector; + +/** + * This function is used to override the remote settings configuration + * if the SEARCH_CONFIG environment variable is set. This allows testing + * against a remote server. + */ +async function maybeSetupConfig() { + const SEARCH_CONFIG = Services.env.get("SEARCH_CONFIG"); + if (SEARCH_CONFIG) { + if (!(SEARCH_CONFIG in SearchUtils.ENGINES_URLS)) { + throw new Error(`Invalid value for SEARCH_CONFIG`); + } + const url = SearchUtils.ENGINES_URLS[SEARCH_CONFIG]; + const response = await fetch(url); + const config = await response.json(); + const settings = await RemoteSettings(SearchUtils.SETTINGS_KEY); + sinon.stub(settings, "get").returns(config.data); + } +} + +/** + * This class implements the test harness for search configuration tests. + * These tests are designed to ensure that the correct search engines are + * loaded for the various region/locale configurations. + * + * The configuration for each test is represented by an object having the + * following properties: + * + * - identifier (string) + * The identifier for the search engine under test. + * - default (object) + * An inclusion/exclusion configuration (see below) to detail when this engine + * should be listed as default. + * + * The inclusion/exclusion configuration is represented as an object having the + * following properties: + * + * - included (array) + * An optional array of region/locale pairs. + * - excluded (array) + * An optional array of region/locale pairs. + * + * If the object is empty, the engine is assumed not to be part of any locale/region + * pair. + * If the object has `excluded` but not `included`, then the engine is assumed to + * be part of every locale/region pair except for where it matches the exclusions. + * + * The region/locale pairs are represented as an object having the following + * properties: + * + * - region (array) + * An array of two-letter region codes. + * - locale (object) + * A locale object which may consist of: + * - matches (array) + * An array of locale strings which should exactly match the locale. + * - startsWith (array) + * An array of locale strings which the locale should start with. + */ +class SearchConfigTest { + /** + * @param {object} config + * The initial configuration for this test, see above. + */ + constructor(config = {}) { + this._config = config; + } + + /** + * Sets up the test. + * + * @param {string} [version] + * The version to simulate for running the tests. + */ + async setup(version = "42.0") { + if (SearchUtils.newSearchConfigEnabled) { + updateAppInfo({ + name: "XPCShell", + ID: "xpcshell@tests.mozilla.org", + version, + platformVersion: version, + }); + } else { + AddonTestUtils.init(GLOBAL_SCOPE); + AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + version, + version + ); + } + + await maybeSetupConfig(); + + // Disable region checks. + Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false); + + // Enable separatePrivateDefault testing. We test with this on, as we have + // separate tests for ensuring the normal = private when this is off. + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + true + ); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); + + if (!SearchUtils.newSearchConfigEnabled) { + await AddonTestUtils.promiseStartupManager(); + } + await Services.search.init(); + + // We must use the engine selector that the search service has created (if + // it has), as remote settings can only easily deal with us loading the + // configuration once - after that, it tries to access the network. + engineSelector = + Services.search.wrappedJSObject._engineSelector || + SearchUtils.newSearchConfigEnabled + ? new SearchEngineSelector() + : new SearchEngineSelectorOld(); + + // Note: we don't use the helper function here, so that we have at least + // one message output per process. + Assert.ok( + Services.search.isInitialized, + "Should have correctly initialized the search service" + ); + } + + /** + * Runs the test. + */ + async run() { + const locales = await this.getLocales(); + const regions = this._regions; + + // We loop on region and then locale, so that we always cause a re-init + // when updating the requested/available locales. + for (let region of regions) { + for (let locale of locales) { + const engines = await this._getEngines(region, locale); + this._assertEngineRules([engines[0]], region, locale, "default"); + const isPresent = this._assertAvailableEngines(region, locale, engines); + if (isPresent) { + this._assertEngineDetails(region, locale, engines); + } + } + } + } + + async _getEngines(region, locale) { + let configs = await engineSelector.fetchEngineConfiguration({ + locale, + region: region || "default", + channel: SearchUtils.MODIFIED_APP_CHANNEL, + }); + + return SearchTestUtils.searchConfigToEngines(configs.engines); + } + + /** + * @returns {Set} the list of regions for the tests to run with. + */ + get _regions() { + // TODO: The legacy configuration worked with null as an unknown region, + // for the search engine selector, we expect "default" but apply the + // fallback in _getEngines. Once we remove the legacy configuration, we can + // simplify this. + if (TEST_DEBUG) { + return new Set(["by", "cn", "kz", "us", "ru", "tr", null]); + } + return [...Services.intl.getAvailableLocaleDisplayNames("region"), null]; + } + + /** + * @returns {Array} the list of locales for the tests to run with. + */ + async getLocales() { + if (TEST_DEBUG) { + return ["be", "en-US", "kk", "tr", "ru", "zh-CN", "ach", "unknown"]; + } + const data = await IOUtils.readUTF8(do_get_file("all-locales").path); + // "en-US" is not in all-locales as it is the default locale + // add it manually to ensure it is tested. + let locales = [...data.split("\n").filter(e => e != ""), "en-US"]; + // BCP47 requires all variants are 5-8 characters long. Our + // build sytem uses the short `mac` variant, this is invalid, and inside + // the app we turn it into `ja-JP-macos` + locales = locales.map(l => (l == "ja-JP-mac" ? "ja-JP-macos" : l)); + // The locale sometimes can be unknown or a strange name, e.g. if the updater + // is disabled, it may be "und", add one here so we know what happens if we + // hit it. + locales.push("unknown"); + return locales; + } + + /** + * Determines if a locale/region pair match a section of the configuration. + * + * @param {object} section + * The configuration section to match against. + * @param {string} region + * The two-letter region code. + * @param {string} locale + * The two-letter locale code. + * @returns {boolean} + * True if the locale/region pair matches the section. + */ + _localeRegionInSection(section, region, locale) { + for (const { regions, locales } of section) { + // If we only specify a regions or locales section then + // it is always considered included in the other section. + const inRegions = !regions || regions.includes(region); + const inLocales = !locales || locales.includes(locale); + if (inRegions && inLocales) { + return true; + } + } + return false; + } + + /** + * Helper function to find an engine from within a list. + * + * @param {Array} engines + * The list of engines to check. + * @param {string} identifier + * The identifier to look for in the list. + * @param {boolean} exactMatch + * Whether to use an exactMatch for the identifier. + * @returns {Engine} + * Returns the engine if found, null otherwise. + */ + _findEngine(engines, identifier, exactMatch) { + return engines.find(engine => + exactMatch + ? engine.identifier == identifier + : engine.identifier.startsWith(identifier) + ); + } + + /** + * Asserts whether the engines rules defined in the configuration are met. + * + * @param {Array} engines + * The list of engines to check. + * @param {string} region + * The two-letter region code. + * @param {string} locale + * The two-letter locale code. + * @param {string} section + * The section of the configuration to check. + * @returns {boolean} + * Returns true if the engine is expected to be present, false otherwise. + */ + _assertEngineRules(engines, region, locale, section) { + const infoString = `region: "${region}" locale: "${locale}"`; + const config = this._config[section]; + const hasIncluded = "included" in config; + const hasExcluded = "excluded" in config; + const identifierIncluded = !!this._findEngine( + engines, + this._config.identifier, + this._config.identifierExactMatch ?? false + ); + + // If there's not included/excluded, then this shouldn't be the default anywhere. + if (section == "default" && !hasIncluded && !hasExcluded) { + this.assertOk( + !identifierIncluded, + `Should not be ${section} for any locale/region, + currently set for ${infoString}` + ); + return false; + } + + // If there's no included section, we assume the engine is default everywhere + // and we should apply the exclusions instead. + let included = + hasIncluded && + this._localeRegionInSection(config.included, region, locale); + + let excluded = + hasExcluded && + this._localeRegionInSection(config.excluded, region, locale); + if ( + (included && (!hasExcluded || !excluded)) || + (!hasIncluded && hasExcluded && !excluded) + ) { + this.assertOk( + identifierIncluded, + `Should be ${section} for ${infoString}` + ); + return true; + } + this.assertOk( + !identifierIncluded, + `Should not be ${section} for ${infoString}` + ); + return false; + } + + /** + * Asserts whether the engine is correctly set as default or not. + * + * @param {string} region + * The two-letter region code. + * @param {string} locale + * The two-letter locale code. + */ + _assertDefaultEngines(region, locale) { + this._assertEngineRules( + [Services.search.appDefaultEngine], + region, + locale, + "default" + ); + // At the moment, this uses the same section as the normal default, as + // we don't set this differently for any region/locale. + this._assertEngineRules( + [Services.search.appPrivateDefaultEngine], + region, + locale, + "default" + ); + } + + /** + * Asserts whether the engine is correctly available or not. + * + * @param {string} region + * The two-letter region code. + * @param {string} locale + * The two-letter locale code. + * @param {Array} engines + * The current visible engines. + * @returns {boolean} + * Returns true if the engine is expected to be present, false otherwise. + */ + _assertAvailableEngines(region, locale, engines) { + return this._assertEngineRules(engines, region, locale, "available"); + } + + /** + * Asserts the engine follows various rules. + * + * @param {string} region + * The two-letter region code. + * @param {string} locale + * The two-letter locale code. + * @param {Array} engines + * The current visible engines. + */ + _assertEngineDetails(region, locale, engines) { + const details = this._config.details.filter(value => { + const included = this._localeRegionInSection( + value.included, + region, + locale + ); + const excluded = + value.excluded && + this._localeRegionInSection(value.excluded, region, locale); + return included && !excluded; + }); + this.assertEqual( + details.length, + 1, + `Should have just one details section for region: ${region} locale: ${locale}` + ); + + const engine = this._findEngine( + engines, + this._config.identifier, + this._config.identifierExactMatch ?? false + ); + this.assertOk(engine, "Should have an engine present"); + + if (this._config.aliases) { + this.assertDeepEqual( + engine.aliases, + this._config.aliases, + "Should have the correct aliases for the engine" + ); + } + + const location = `in region:${region}, locale:${locale}`; + + for (const rule of details) { + this._assertCorrectDomains(location, engine, rule); + if (rule.codes) { + this._assertCorrectCodes(location, engine, rule); + } + if (rule.searchUrlCode || rule.suggestUrlCode) { + this._assertCorrectUrlCode(location, engine, rule); + } + if (rule.aliases) { + this.assertDeepEqual( + engine.aliases, + rule.aliases, + "Should have the correct aliases for the engine" + ); + } + if (rule.telemetryId) { + this.assertEqual( + engine.telemetryId, + rule.telemetryId, + `Should have the correct telemetryId ${location}.` + ); + } + } + } + + /** + * Asserts whether the engine is using the correct domains or not. + * + * @param {string} location + * Debug string with locale + region information. + * @param {object} engine + * The engine being tested. + * @param {object} rules + * Rules to test. + */ + _assertCorrectDomains(location, engine, rules) { + this.assertOk( + rules.domain, + `Should have an expectedDomain for the engine ${location}` + ); + + const searchForm = new URL(engine.searchForm); + this.assertOk( + searchForm.host.endsWith(rules.domain), + `Should have the correct search form domain ${location}. + Got "${searchForm.host}", expected to end with "${rules.domain}".` + ); + + let submission = engine.getSubmission("test", URLTYPE_SEARCH_HTML); + + this.assertOk( + submission.uri.host.endsWith(rules.domain), + `Should have the correct domain for type: ${URLTYPE_SEARCH_HTML} ${location}. + Got "${submission.uri.host}", expected to end with "${rules.domain}".` + ); + + submission = engine.getSubmission("test", URLTYPE_SUGGEST_JSON); + if (this._config.noSuggestionsURL || rules.noSuggestionsURL) { + this.assertOk(!submission, "Should not have a submission url"); + } else if (this._config.suggestionUrlBase) { + this.assertEqual( + submission.uri.prePath + submission.uri.filePath, + this._config.suggestionUrlBase, + `Should have the correct domain for type: ${URLTYPE_SUGGEST_JSON} ${location}.` + ); + this.assertOk( + submission.uri.query.includes(rules.suggestUrlCode), + `Should have the code in the uri` + ); + } + } + + /** + * Asserts whether the engine is using the correct codes or not. + * + * @param {string} location + * Debug string with locale + region information. + * @param {object} engine + * The engine being tested. + * @param {object} rules + * Rules to test. + */ + _assertCorrectCodes(location, engine, rules) { + for (const purpose of SUBMISSION_PURPOSES) { + // Don't need to repeat the code if we use it for all purposes. + const code = + typeof rules.codes === "string" ? rules.codes : rules.codes[purpose]; + const submission = engine.getSubmission("test", "text/html", purpose); + const submissionQueryParams = submission.uri.query.split("&"); + this.assertOk( + submissionQueryParams.includes(code), + `Expected "${code}" in url "${submission.uri.spec}" from purpose "${purpose}" ${location}` + ); + + const paramName = code.split("=")[0]; + this.assertOk( + submissionQueryParams.filter(param => param.startsWith(paramName)) + .length == 1, + `Expected only one "${paramName}" parameter in "${submission.uri.spec}" from purpose "${purpose}" ${location}` + ); + } + } + + /** + * Asserts whether the engine is using the correct URL codes or not. + * + * @param {string} location + * Debug string with locale + region information. + * @param {object} engine + * The engine being tested. + * @param {object} rule + * Rules to test. + */ + _assertCorrectUrlCode(location, engine, rule) { + if (rule.searchUrlCode) { + const submission = engine.getSubmission("test", URLTYPE_SEARCH_HTML); + this.assertOk( + submission.uri.query.split("&").includes(rule.searchUrlCode), + `Expected "${rule.searchUrlCode}" in search url "${submission.uri.spec}"` + ); + let uri = engine.searchForm; + this.assertOk( + !uri.includes(rule.searchUrlCode), + `"${rule.searchUrlCode}" should not be in the search form URL.` + ); + } + if (rule.searchUrlCodeNotInQuery) { + const submission = engine.getSubmission("test", URLTYPE_SEARCH_HTML); + this.assertOk( + submission.uri.includes(rule.searchUrlCodeNotInQuery), + `Expected "${rule.searchUrlCodeNotInQuery}" in search url "${submission.uri.spec}"` + ); + } + if (rule.suggestUrlCode) { + const submission = engine.getSubmission("test", URLTYPE_SUGGEST_JSON); + this.assertOk( + submission.uri.query.split("&").includes(rule.suggestUrlCode), + `Expected "${rule.suggestUrlCode}" in suggestion url "${submission.uri.spec}"` + ); + } + } + + /** + * Helper functions which avoid outputting test results when there are no + * failures. These help the tests to run faster, and avoid clogging up the + * python test runner process. + */ + + assertOk(value, message) { + if (!value || TEST_DEBUG) { + Assert.ok(value, message); + } + } + + assertEqual(actual, expected, message) { + if (actual != expected || TEST_DEBUG) { + Assert.equal(actual, expected, message); + } + } + + assertDeepEqual(actual, expected, message) { + if (!ObjectUtils.deepEqual(actual, expected)) { + Assert.deepEqual(actual, expected, message); + } + } +} + +async function checkUISchemaValid(configSchema, uiSchema) { + for (let key of Object.keys(configSchema.properties)) { + Assert.ok( + uiSchema["ui:order"].includes(key), + `Should have ${key} listed at the top-level of the ui schema` + ); + } +} diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_amazon.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_amazon.js new file mode 100644 index 0000000000..bfe05e1596 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_amazon.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const test = new SearchConfigTest({ + identifier: "amazon", + default: { + // Not default anywhere. + }, + available: { + included: [ + { + // The main regions we ship Amazon to. Below this are special cases. + regions: ["us", "jp"], + }, + ], + }, + details: [ + { + domain: "amazon.co.jp", + telemetryId: "amazon-jp", + aliases: ["@amazon"], + included: [ + { + regions: ["jp"], + }, + ], + searchUrlCode: "tag=mozillajapan-fx-22", + noSuggestionsURL: true, + }, + { + domain: "amazon.com", + telemetryId: "amazondotcom-us-adm", + aliases: ["@amazon"], + included: [ + { + regions: ["us"], + }, + ], + noSuggestionsURL: true, + searchUrlCode: "tag=admarketus-20", + }, + ], +}); + +add_setup(async function () { + // We only need to do setup on one of the tests. + await test.setup("89.0"); +}); + +add_task(async function test_searchConfig_amazon() { + await test.run(); +}); + +add_task( + { skip_if: () => SearchUtils.newSearchConfigEnabled }, + async function test_searchConfig_amazon_pre89() { + const version = "88.0"; + AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + version, + version + ); + // For pre-89, Amazon has a slightly different config. + let details = test._config.details.find( + d => d.telemetryId == "amazondotcom-us-adm" + ); + details.telemetryId = "amazondotcom"; + delete details.searchUrlCode; + + await test.run(); + } +); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_baidu.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_baidu.js new file mode 100644 index 0000000000..3c66708bdc --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_baidu.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const test = new SearchConfigTest({ + identifier: "baidu", + aliases: ["@\u767E\u5EA6", "@baidu"], + default: { + included: [ + { + regions: ["cn"], + locales: ["zh-CN"], + }, + ], + }, + available: { + included: [ + { + locales: ["zh-CN"], + }, + ], + }, + details: [ + { + included: [{}], + domain: "baidu.com", + telemetryId: "baidu", + }, + ], +}); + +add_setup(async function () { + await test.setup(); +}); + +add_task(async function test_searchConfig_baidu() { + await test.run(); +}); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_bing.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_bing.js new file mode 100644 index 0000000000..6dbddd8a08 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_bing.js @@ -0,0 +1,134 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const test = new SearchConfigTest({ + identifier: "bing", + aliases: ["@bing"], + default: { + // Not included anywhere. + }, + available: { + included: [ + { + // regions: [ + // These arent currently enforced. + // "au", "at", "be", "br", "ca", "fi", "fr", "de", + // "in", "ie", "it", "jp", "my", "mx", "nl", "nz", + // "no", "sg", "es", "se", "ch", "gb", "us", + // ], + locales: [ + "ach", + "af", + "an", + "ar", + "ast", + "az", + "bn", + "bs", + "ca", + "ca-valencia", + "cak", + "cs", + "cy", + "da", + "de", + "dsb", + "el", + "en-CA", + "en-GB", + "en-US", + "eo", + "es-CL", + "es-ES", + "es-MX", + "eu", + "fa", + "ff", + "fi", + "fr", + "fur", + "fy-NL", + "gd", + "gl", + "gn", + "gu-IN", + "he", + "hi-IN", + "hr", + "hsb", + "hy-AM", + "ia", + "id", + "is", + "it", + "ja-JP-macos", + "ja", + "ka", + "kab", + "km", + "kn", + "lij", + "lo", + "lt", + "meh", + "mk", + "ms", + "my", + "nb-NO", + "ne-NP", + "nl", + "nn-NO", + "oc", + "pa-IN", + "pt-BR", + "rm", + "ro", + "sc", + "sco", + "son", + "sq", + "sr", + "sv-SE", + "te", + "th", + "tl", + "tr", + "trs", + "uk", + "ur", + "uz", + "wo", + "xh", + "zh-CN", + ], + }, + ], + }, + details: [ + { + included: [{}], + domain: "bing.com", + telemetryId: + SearchUtils.MODIFIED_APP_CHANNEL == "esr" ? "bing-esr" : "bing", + codes: { + searchbar: "form=MOZSBR", + keyword: "form=MOZLBR", + contextmenu: "form=MOZCON", + homepage: "form=MOZSPG", + newtab: "form=MOZTSB", + }, + searchUrlCode: + SearchUtils.MODIFIED_APP_CHANNEL == "esr" ? "pc=MOZR" : "pc=MOZI", + }, + ], +}); + +add_setup(async function () { + await test.setup(); +}); + +add_task(async function test_searchConfig_bing() { + await test.run(); +}); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_distributions.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_distributions.js new file mode 100644 index 0000000000..513fdaafef --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_distributions.js @@ -0,0 +1,348 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SearchService: "resource://gre/modules/SearchService.sys.mjs", +}); + +const tests = []; + +for (let canonicalId of ["canonical", "canonical-001"]) { + tests.push({ + locale: "en-US", + region: "US", + distribution: canonicalId, + test: engines => + hasParams(engines, "Google", "searchbar", "client=ubuntu") && + hasParams(engines, "Google", "searchbar", "channel=fs") && + hasTelemetryId(engines, "Google", "google-canonical"), + }); + + tests.push({ + locale: "en-US", + region: "GB", + distribution: canonicalId, + test: engines => + hasParams(engines, "Google", "searchbar", "client=ubuntu") && + hasParams(engines, "Google", "searchbar", "channel=fs") && + hasTelemetryId(engines, "Google", "google-canonical"), + }); +} + +tests.push({ + locale: "en-US", + region: "US", + distribution: "canonical-002", + test: engines => + hasParams(engines, "Google", "searchbar", "client=ubuntu-sn") && + hasParams(engines, "Google", "searchbar", "channel=fs") && + hasTelemetryId(engines, "Google", "google-ubuntu-sn"), +}); + +tests.push({ + locale: "en-US", + region: "GB", + distribution: "canonical-002", + test: engines => + hasParams(engines, "Google", "searchbar", "client=ubuntu-sn") && + hasParams(engines, "Google", "searchbar", "channel=fs") && + hasTelemetryId(engines, "Google", "google-ubuntu-sn"), +}); + +tests.push({ + locale: "zh-CN", + region: "CN", + distribution: "MozillaOnline", + test: engines => + hasEnginesFirst(engines, ["百度", "Bing", "Google", "维基百科"]), +}); + +tests.push({ + locale: "fr", + distribution: "qwant-001", + test: engines => + hasParams(engines, "Qwant", "searchbar", "client=firefoxqwant") && + hasDefault(engines, "Qwant") && + hasEnginesFirst(engines, ["Qwant", "Qwant Junior"]), +}); + +tests.push({ + locale: "fr", + distribution: "qwant-001", + test: engines => + hasParams(engines, "Qwant Junior", "searchbar", "client=firefoxqwant"), +}); + +tests.push({ + locale: "fr", + distribution: "qwant-002", + test: engines => + hasParams(engines, "Qwant", "searchbar", "client=firefoxqwant") && + hasDefault(engines, "Qwant") && + hasEnginesFirst(engines, ["Qwant", "Qwant Junior"]), +}); + +tests.push({ + locale: "fr", + distribution: "qwant-002", + test: engines => + hasParams(engines, "Qwant Junior", "searchbar", "client=firefoxqwant"), +}); + +for (const locale of ["en-US", "de"]) { + tests.push({ + locale, + distribution: "1und1", + test: engines => + hasParams(engines, "1&1 Suche", "searchbar", "enc=UTF-8") && + hasDefault(engines, "1&1 Suche") && + hasEnginesFirst(engines, ["1&1 Suche"]), + }); + + tests.push({ + locale, + distribution: "gmx", + test: engines => + hasParams(engines, "GMX Suche", "searchbar", "enc=UTF-8") && + hasDefault(engines, "GMX Suche") && + hasEnginesFirst(engines, ["GMX Suche"]), + }); + + tests.push({ + locale, + distribution: "gmx", + test: engines => + hasParams(engines, "GMX Shopping", "searchbar", "origin=br_osd"), + }); + + tests.push({ + locale, + distribution: "mail.com", + test: engines => + hasParams(engines, "mail.com search", "searchbar", "enc=UTF-8") && + hasDefault(engines, "mail.com search") && + hasEnginesFirst(engines, ["mail.com search"]), + }); + + tests.push({ + locale, + distribution: "webde", + test: engines => + hasParams(engines, "WEB.DE Suche", "searchbar", "enc=UTF-8") && + hasDefault(engines, "WEB.DE Suche") && + hasEnginesFirst(engines, ["WEB.DE Suche"]), + }); +} + +tests.push({ + locale: "ru", + region: "RU", + distribution: "gmx", + test: engines => hasDefault(engines, "GMX Suche"), +}); + +tests.push({ + locale: "en-GB", + distribution: "gmxcouk", + test: engines => + hasURLs( + engines, + "GMX Search", + "https://go.gmx.co.uk/br/moz_search_web/?enc=UTF-8&q=test", + SearchUtils.newSearchConfigEnabled + ? "https://suggestplugin.gmx.co.uk/s?brand=gmxcouk&origin=moz_splugin_ff&enc=UTF-8&q=test" + : "https://suggestplugin.gmx.co.uk/s?q=test&brand=gmxcouk&origin=moz_splugin_ff&enc=UTF-8" + ) && + hasDefault(engines, "GMX Search") && + hasEnginesFirst(engines, ["GMX Search"]), +}); + +tests.push({ + locale: "ru", + region: "RU", + distribution: "gmxcouk", + test: engines => hasDefault(engines, "GMX Search"), +}); + +tests.push({ + locale: "es", + distribution: "gmxes", + test: engines => + hasURLs( + engines, + "GMX - Búsqueda web", + "https://go.gmx.es/br/moz_search_web/?enc=UTF-8&q=test", + SearchUtils.newSearchConfigEnabled + ? "https://suggestplugin.gmx.es/s?brand=gmxes&origin=moz_splugin_ff&enc=UTF-8&q=test" + : "https://suggestplugin.gmx.es/s?q=test&brand=gmxes&origin=moz_splugin_ff&enc=UTF-8" + ) && + hasDefault(engines, "GMX Search") && + hasEnginesFirst(engines, ["GMX Search"]), +}); + +tests.push({ + locale: "ru", + region: "RU", + distribution: "gmxes", + test: engines => hasDefault(engines, "GMX - Búsqueda web"), +}); + +tests.push({ + locale: "fr", + distribution: "gmxfr", + test: engines => + hasURLs( + engines, + "GMX - Recherche web", + "https://go.gmx.fr/br/moz_search_web/?enc=UTF-8&q=test", + SearchUtils.newSearchConfigEnabled + ? "https://suggestplugin.gmx.fr/s?brand=gmxfr&origin=moz_splugin_ff&enc=UTF-8&q=test" + : "https://suggestplugin.gmx.fr/s?q=test&brand=gmxfr&origin=moz_splugin_ff&enc=UTF-8" + ) && + hasDefault(engines, "GMX Search") && + hasEnginesFirst(engines, ["GMX Search"]), +}); + +tests.push({ + locale: "ru", + region: "RU", + distribution: "gmxfr", + test: engines => hasDefault(engines, "GMX - Recherche web"), +}); + +tests.push({ + locale: "en-US", + region: "US", + distribution: "mint-001", + test: engines => + hasParams(engines, "DuckDuckGo", "searchbar", "t=lm") && + hasParams(engines, "Google", "searchbar", "client=firefox-b-1-lm") && + hasDefault(engines, "Google") && + hasEnginesFirst(engines, ["Google"]) && + hasTelemetryId(engines, "Google", "google-b-1-lm"), +}); + +tests.push({ + locale: "en-GB", + region: "GB", + distribution: "mint-001", + test: engines => + hasParams(engines, "DuckDuckGo", "searchbar", "t=lm") && + hasParams(engines, "Google", "searchbar", "client=firefox-b-lm") && + hasDefault(engines, "Google") && + hasEnginesFirst(engines, ["Google"]) && + hasTelemetryId(engines, "Google", "google-b-lm"), +}); + +function hasURLs(engines, engineName, url, suggestURL) { + let engine = engines.find(e => e._name === engineName); + Assert.ok(engine, `Should be able to find ${engineName}`); + + let submission = engine.getSubmission("test", "text/html"); + Assert.equal( + submission.uri.spec, + url, + `Should have the correct submission url for ${engineName}` + ); + + submission = engine.getSubmission("test", "application/x-suggestions+json"); + Assert.equal( + submission.uri.spec, + suggestURL, + `Should have the correct suggestion url for ${engineName}` + ); +} + +function hasParams(engines, engineName, purpose, param) { + let engine = engines.find(e => e._name === engineName); + Assert.ok(engine, `Should be able to find ${engineName}`); + + let submission = engine.getSubmission("test", "text/html", purpose); + let queries = submission.uri.query.split("&"); + + let paramNames = new Set(); + for (let query of queries) { + let queryParam = query.split("=")[0]; + Assert.ok( + !paramNames.has(queryParam), + `Should not have a duplicate ${queryParam} param` + ); + paramNames.add(queryParam); + } + + let result = queries.includes(param); + Assert.ok(result, `expect ${submission.uri.query} to include ${param}`); + return true; +} + +function hasTelemetryId(engines, engineName, telemetryId) { + let engine = engines.find(e => e._name === engineName); + Assert.ok(engine, `Should be able to find ${engineName}`); + + Assert.equal( + engine.telemetryId, + telemetryId, + "Should have the correct telemetryId" + ); + return true; +} + +function hasDefault(engines, expectedDefaultName) { + Assert.equal( + engines[0].name, + expectedDefaultName, + "Should have the expected engine set as default" + ); + return true; +} + +function hasEnginesFirst(engines, expectedEngines) { + for (let [i, expectedEngine] of expectedEngines.entries()) { + Assert.equal( + engines[i].name, + expectedEngine, + `Should have the expected engine in position ${i}` + ); + } +} + +engineSelector = SearchUtils.newSearchConfigEnabled + ? new SearchEngineSelector() + : new SearchEngineSelectorOld(); + +add_setup(async function () { + if (!SearchUtils.newSearchConfigEnabled) { + AddonTestUtils.init(GLOBAL_SCOPE); + AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" + ); + await AddonTestUtils.promiseStartupManager(); + } + + await maybeSetupConfig(); +}); + +add_task(async function test_expected_distribution_engines() { + let searchService = new SearchService(); + for (const { distribution, locale = "en-US", region = "US", test } of tests) { + let config = await engineSelector.fetchEngineConfiguration({ + locale, + region, + distroID: distribution, + }); + let engines = await SearchTestUtils.searchConfigToEngines(config.engines); + searchService._engines = engines; + searchService._searchDefault = { + id: config.engines[0].webExtension.id, + locale: + config.engines[0]?.webExtension?.locale ?? SearchUtils.DEFAULT_TAG, + }; + engines = searchService._sortEnginesByDefaults(engines); + test(engines); + } +}); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_duckduckgo.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_duckduckgo.js new file mode 100644 index 0000000000..ffbd3fb1ce --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_duckduckgo.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const test = new SearchConfigTest({ + identifier: "ddg", + aliases: ["@duckduckgo", "@ddg"], + default: { + // Not included anywhere. + }, + available: { + excluded: [ + // Should be available everywhere. + ], + }, + details: [ + { + included: [{}], + domain: "duckduckgo.com", + telemetryId: + SearchUtils.MODIFIED_APP_CHANNEL == "esr" ? "ddg-esr" : "ddg", + searchUrlCode: + SearchUtils.MODIFIED_APP_CHANNEL == "esr" ? "t=ftsa" : "t=ffab", + }, + ], +}); + +add_setup(async function () { + await test.setup(); +}); + +add_task(async function test_searchConfig_duckduckgo() { + await test.run(); +}); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_ebay.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_ebay.js new file mode 100644 index 0000000000..ed8e5147ee --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_ebay.js @@ -0,0 +1,276 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOMAIN_LOCALES = { + "ebay-ca": ["en-CA"], + "ebay-ch": ["rm"], + "ebay-de": ["de", "dsb", "hsb"], + "ebay-es": ["an", "ast", "ca", "ca-valencia", "es-ES", "eu", "gl"], + "ebay-ie": ["ga-IE", "ie"], + "ebay-it": ["fur", "it", "lij", "sc"], + "ebay-nl": ["fy-NL", "nl"], + "ebay-uk": ["cy", "en-GB", "gd"], +}; + +const test = new SearchConfigTest({ + identifier: "ebay", + aliases: ["@ebay"], + default: { + // Not included anywhere. + }, + available: { + included: [ + { + // We don't currently enforce by region, but do locale instead. + // regions: [ + // "us", "gb", "ca", "ie", "fr", "it", "de", "at", "es", "nl", "ch", "au" + // ], + locales: [ + "an", + "ast", + "br", + "ca", + "ca-valencia", + "cy", + "de", + "dsb", + "en-CA", + "en-GB", + "es-ES", + "eu", + "fur", + "fr", + "fy-NL", + "ga-IE", + "gd", + "gl", + "hsb", + "it", + "lij", + "nl", + "rm", + "sc", + "wo", + ], + }, + { + regions: ["au", "be", "ca", "ch", "gb", "ie", "nl", "us"], + locales: ["en-US"], + }, + { + regions: ["gb"], + locales: ["sco"], + }, + ], + }, + suggestionUrlBase: "https://autosug.ebay.com/autosug", + details: [ + { + // Note: These should be based on region, but we don't currently enforce that. + // Note: the order here is important. A region/locale match higher up in the + // list will override a region/locale match lower down. + domain: "www.befr.ebay.be", + telemetryId: "ebay-be", + included: [ + { + regions: ["be"], + locales: ["br", "unknown", "en-US", "fr", "fy-NL", "nl", "wo"], + }, + ], + searchUrlCode: "mkrid=1553-53471-19255-0", + suggestUrlCode: "sId=23", + }, + { + domain: "www.ebay.at", + telemetryId: "ebay-at", + included: [ + { + regions: ["at"], + locales: ["de", "dsb", "hsb"], + }, + ], + searchUrlCode: "mkrid=5221-53469-19255-0", + suggestUrlCode: "sId=16", + }, + { + domain: "www.ebay.ca", + telemetryId: "ebay-ca", + included: [ + { + locales: DOMAIN_LOCALES["ebay-ca"], + }, + { + regions: ["ca"], + }, + ], + excluded: [ + { + locales: [ + ...DOMAIN_LOCALES["ebay-ch"], + ...DOMAIN_LOCALES["ebay-de"], + ...DOMAIN_LOCALES["ebay-es"], + ...DOMAIN_LOCALES["ebay-ie"], + ...DOMAIN_LOCALES["ebay-it"], + ...DOMAIN_LOCALES["ebay-nl"], + ...DOMAIN_LOCALES["ebay-uk"], + ], + }, + ], + searchUrlCode: "mkrid=706-53473-19255-0", + suggestUrlCode: "sId=2", + }, + { + domain: "www.ebay.ch", + telemetryId: "ebay-ch", + included: [ + { + locales: DOMAIN_LOCALES["ebay-ch"], + }, + { + regions: ["ch"], + }, + ], + excluded: [ + { + locales: [ + ...DOMAIN_LOCALES["ebay-ca"], + ...DOMAIN_LOCALES["ebay-es"], + ...DOMAIN_LOCALES["ebay-ie"], + ...DOMAIN_LOCALES["ebay-it"], + ...DOMAIN_LOCALES["ebay-nl"], + ...DOMAIN_LOCALES["ebay-uk"], + ], + }, + ], + searchUrlCode: "mkrid=5222-53480-19255-0", + suggestUrlCode: "sId=193", + }, + { + domain: "www.ebay.com", + telemetryId: "ebay", + included: [ + { + locales: ["unknown", "en-US"], + }, + ], + excluded: [{ regions: ["au", "be", "ca", "ch", "gb", "ie", "nl"] }], + searchUrlCode: "mkrid=711-53200-19255-0", + suggestUrlCode: "sId=0", + }, + { + domain: "www.ebay.com.au", + telemetryId: "ebay-au", + included: [ + { + regions: ["au"], + locales: ["cy", "unknown", "en-GB", "en-US", "gd"], + }, + ], + searchUrlCode: "mkrid=705-53470-19255-0", + suggestUrlCode: "sId=15", + }, + { + domain: "www.ebay.ie", + telemetryId: "ebay-ie", + included: [ + { + locales: DOMAIN_LOCALES["ebay-ie"], + }, + { + regions: ["ie"], + locales: ["cy", "unknown", "en-GB", "en-US", "gd"], + }, + ], + searchUrlCode: "mkrid=5282-53468-19255-0", + suggestUrlCode: "sId=205", + }, + { + domain: "www.ebay.co.uk", + telemetryId: "ebay-uk", + included: [ + { + locales: DOMAIN_LOCALES["ebay-uk"], + }, + { + locales: ["unknown", "en-US", "sco"], + regions: ["gb"], + }, + ], + excluded: [{ regions: ["au", "ie"] }], + searchUrlCode: "mkrid=710-53481-19255-0", + suggestUrlCode: "sId=3", + }, + { + domain: "www.ebay.de", + telemetryId: "ebay-de", + included: [ + { + locales: DOMAIN_LOCALES["ebay-de"], + }, + ], + excluded: [{ regions: ["at", "ch"] }], + searchUrlCode: "mkrid=707-53477-19255-0", + suggestUrlCode: "sId=77", + }, + { + domain: "www.ebay.es", + telemetryId: "ebay-es", + included: [ + { + locales: DOMAIN_LOCALES["ebay-es"], + }, + ], + searchUrlCode: "mkrid=1185-53479-19255-0", + suggestUrlCode: "sId=186", + }, + { + domain: "www.ebay.fr", + telemetryId: "ebay-fr", + included: [ + { + locales: ["br", "fr", "wo"], + }, + ], + excluded: [{ regions: ["be", "ca", "ch"] }], + searchUrlCode: "mkrid=709-53476-19255-0", + suggestUrlCode: "sId=71", + }, + { + domain: "www.ebay.it", + telemetryId: "ebay-it", + included: [ + { + locales: DOMAIN_LOCALES["ebay-it"], + }, + ], + searchUrlCode: "mkrid=724-53478-19255-0", + suggestUrlCode: "sId=101", + }, + { + domain: "www.ebay.nl", + telemetryId: "ebay-nl", + included: [ + { + locales: DOMAIN_LOCALES["ebay-nl"], + }, + { + locales: ["unknown", "en-US"], + regions: ["nl"], + }, + ], + excluded: [{ regions: ["be"] }], + searchUrlCode: "mkrid=1346-53482-19255-0", + suggestUrlCode: "sId=146", + }, + ], +}); + +add_setup(async function () { + await test.setup(); +}); + +add_task(async function test_searchConfig_ebay() { + await test.run(); +}); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_ecosia.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_ecosia.js new file mode 100644 index 0000000000..e9fc7241b1 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_ecosia.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const test = new SearchConfigTest({ + identifier: "ecosia", + aliases: [], + default: { + // Not default anywhere. + }, + available: { + included: [ + { + locales: ["de"], + }, + ], + }, + details: [ + { + included: [{}], + domain: "www.ecosia.org", + telemetryId: "ecosia", + searchUrlCode: "tt=mzl", + }, + ], +}); + +add_setup(async function () { + await test.setup(); +}); + +add_task(async function test_searchConfig_ecosia() { + await test.run(); +}); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_google.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_google.js new file mode 100644 index 0000000000..c6b9d6a991 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_google.js @@ -0,0 +1,171 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", +}); + +const test = new SearchConfigTest({ + identifier: "google", + aliases: ["@google"], + default: { + // Included everywhere apart from the exclusions below. These are basically + // just excluding what Yandex and Baidu include. + excluded: [ + { + regions: ["cn"], + locales: ["zh-CN"], + }, + ], + }, + available: { + excluded: [ + // Should be available everywhere. + ], + }, + details: [ + { + included: [{ regions: ["us"] }], + domain: "google.com", + telemetryId: + SearchUtils.MODIFIED_APP_CHANNEL == "esr" + ? "google-b-1-e" + : "google-b-1-d", + codes: + SearchUtils.MODIFIED_APP_CHANNEL == "esr" + ? "client=firefox-b-1-e" + : "client=firefox-b-1-d", + }, + { + excluded: [{ regions: ["us", "by", "kz", "ru", "tr"] }], + included: [{}], + domain: "google.com", + telemetryId: + SearchUtils.MODIFIED_APP_CHANNEL == "esr" ? "google-b-e" : "google-b-d", + codes: + SearchUtils.MODIFIED_APP_CHANNEL == "esr" + ? "client=firefox-b-e" + : "client=firefox-b-d", + }, + { + included: [{ regions: ["by", "kz", "ru", "tr"] }], + domain: "google.com", + telemetryId: "google-com-nocodes", + }, + ], +}); + +add_setup(async function () { + sinon.spy(NimbusFeatures.search, "onUpdate"); + sinon.stub(NimbusFeatures.search, "ready").resolves(); + await test.setup(); +}); + +add_task(async function test_searchConfig_google() { + await test.run(); +}); + +add_task(async function test_searchConfig_google_with_mozparam() { + // Test a couple of configurations with a MozParam set up. + const TEST_DATA = [ + { + locale: "en-US", + region: "US", + pref: "google_channel_us", + expected: "us_param", + }, + { + locale: "en-US", + region: "GB", + pref: "google_channel_row", + expected: "row_param", + }, + ]; + + const defaultBranch = Services.prefs.getDefaultBranch( + SearchUtils.BROWSER_SEARCH_PREF + ); + for (const testData of TEST_DATA) { + defaultBranch.setCharPref("param." + testData.pref, testData.expected); + } + + for (const testData of TEST_DATA) { + info(`Checking region ${testData.region}, locale ${testData.locale}`); + const engines = await test._getEngines(testData.region, testData.locale); + + Assert.ok( + engines[0].identifier.startsWith("google"), + "Should have the correct engine" + ); + console.log(engines[0]); + + const submission = engines[0].getSubmission("test", URLTYPE_SEARCH_HTML); + Assert.ok( + submission.uri.query.split("&").includes("channel=" + testData.expected), + "Should be including the correct MozParam parameter for the engine" + ); + } + + // Reset the pref values for next tests + for (const testData of TEST_DATA) { + defaultBranch.setCharPref("param." + testData.pref, ""); + } +}); + +add_task(async function test_searchConfig_google_with_nimbus() { + let sandbox = sinon.createSandbox(); + // Test a couple of configurations with a MozParam set up. + const TEST_DATA = [ + { + locale: "en-US", + region: "US", + expected: "nimbus_us_param", + }, + { + locale: "en-US", + region: "GB", + expected: "nimbus_row_param", + }, + ]; + + Assert.ok( + NimbusFeatures.search.onUpdate.called, + "Should register an update listener for Nimbus experiments" + ); + // Stub getVariable to populate the cache with our expected data + sandbox.stub(NimbusFeatures.search, "getVariable").returns([ + { key: "google_channel_us", value: "nimbus_us_param" }, + { key: "google_channel_row", value: "nimbus_row_param" }, + ]); + // Set the pref cache with Nimbus values + NimbusFeatures.search.onUpdate.firstCall.args[0](); + + for (const testData of TEST_DATA) { + info(`Checking region ${testData.region}, locale ${testData.locale}`); + const engines = await test._getEngines(testData.region, testData.locale); + + Assert.ok( + engines[0].identifier.startsWith("google"), + "Should have the correct engine" + ); + console.log(engines[0]); + + const submission = engines[0].getSubmission("test", URLTYPE_SEARCH_HTML); + Assert.ok( + NimbusFeatures.search.ready.called, + "Should wait for Nimbus to get ready" + ); + Assert.ok( + NimbusFeatures.search.getVariable, + "Should call NimbusFeatures.search.getVariable to populate the cache" + ); + Assert.ok( + submission.uri.query.split("&").includes("channel=" + testData.expected), + "Should be including the correct MozParam parameter for the engine" + ); + } + + sandbox.restore(); +}); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_mailru.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_mailru.js new file mode 100644 index 0000000000..e1a9c66b12 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_mailru.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const test = new SearchConfigTest({ + identifier: "mailru", + aliases: [], + default: { + // Not default anywhere. + }, + available: { + included: [ + { + locales: ["ru"], + }, + ], + }, + details: [ + { + included: [{}], + domain: "go.mail.ru", + telemetryId: "mailru", + codes: "gp=900200", + searchUrlCode: "frc=900200", + }, + ], +}); + +add_setup(async function () { + await test.setup(); +}); + +add_task(async function test_searchConfig_mailru() { + await test.run(); +}).skip(); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_qwant.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_qwant.js new file mode 100644 index 0000000000..4024385729 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_qwant.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const test = new SearchConfigTest({ + identifier: "qwant", + aliases: ["@qwant"], + default: { + // Not default anywhere. + }, + available: { + included: [ + { + locales: ["fr"], + }, + ], + }, + details: [ + { + included: [{}], + domain: "www.qwant.com", + telemetryId: "qwant", + searchUrlCode: "client=brz-moz", + suggestUrlCode: "client=opensearch", + }, + ], +}); + +add_setup(async function () { + await test.setup(); +}); + +add_task(async function test_searchConfig_qwant() { + await test.run(); +}); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_rakuten.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_rakuten.js new file mode 100644 index 0000000000..2c0010e2b3 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_rakuten.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const test = new SearchConfigTest({ + identifier: "rakuten", + aliases: [], + default: { + // Not default anywhere. + }, + available: { + included: [ + { + locales: ["ja", "ja-JP-macos"], + }, + ], + }, + details: [ + { + included: [{}], + domain: "rakuten.co.jp", + telemetryId: "rakuten", + searchUrlCodeNotInQuery: "013ca98b.cd7c5f0c", + }, + ], +}); + +add_setup(async function () { + await test.setup(); +}); + +add_task(async function test_searchConfig_rakuten() { + await test.run(); +}); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchconfig_validates.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchconfig_validates.js new file mode 100644 index 0000000000..51e71ff573 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchconfig_validates.js @@ -0,0 +1,209 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs", + SearchEngineSelectorOld: + "resource://gre/modules/SearchEngineSelectorOld.sys.mjs", +}); + +/** + * Checks to see if a value is an object or not. + * + * @param {*} value + * The value to check. + * @returns {boolean} + */ +function isObject(value) { + return value != null && typeof value == "object" && !Array.isArray(value); +} + +/** + * This function modifies the schema to prevent allowing additional properties + * on objects. This is used to enforce that the schema contains everything that + * we deliver via the search configuration. + * + * These checks are not enabled in-product, as we want to allow older versions + * to keep working if we add new properties for whatever reason. + * + * @param {object} section + * The section to check to see if an additionalProperties flag should be added. + */ +function disallowAdditionalProperties(section) { + // It is generally acceptable for new properties to be added to the + // configuration as older builds will ignore them. + // + // As a result, we only check for new properties on nightly builds, and this + // avoids us having to uplift schema changes. This also helps preserve the + // schemas as documentation of "what was supported in this version". + if (!AppConstants.NIGHTLY_BUILD) { + return; + } + + // If the section is a `oneOf` section, avoid the additionalProperties check. + // Otherwise, the validator expects all properties of any `oneOf` item to be + // present. + if (isObject(section)) { + if (section.properties && !("recordType" in section.properties)) { + section.additionalProperties = false; + } + if ("then" in section) { + section.then.additionalProperties = false; + } + } + + for (let value of Object.values(section)) { + if (isObject(value)) { + disallowAdditionalProperties(value); + } else if (Array.isArray(value)) { + for (let item of value) { + disallowAdditionalProperties(item); + } + } + } +} + +let searchConfigSchemaV1; +let searchConfigSchema; + +add_setup(async function () { + searchConfigSchemaV1 = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, "search-config-schema.json") + ); + searchConfigSchema = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, "search-config-v2-schema.json") + ); +}); + +async function checkSearchConfigValidates(schema, searchConfig) { + disallowAdditionalProperties(schema); + let validator = new JsonSchema.Validator(schema); + + for (let entry of searchConfig) { + // Records in Remote Settings contain additional properties independent of + // the schema. Hence, we don't want to validate their presence. + delete entry.schema; + delete entry.id; + delete entry.last_modified; + + let result = validator.validate(entry); + // entry.webExtension.id supports search-config v1. + let message = `Should validate ${ + entry.identifier ?? entry.recordType ?? entry.webExtension.id + }`; + if (!result.valid) { + message += `:\n${JSON.stringify(result.errors, null, 2)}`; + } + Assert.ok(result.valid, message); + + // All engine objects should have the base URL defined for each entry in + // entry.base.urls. + // Unfortunately this is difficult to enforce in the schema as it would + // need a `required` field that works across multiple levels. + if (entry.recordType == "engine") { + for (let urlEntry of Object.values(entry.base.urls)) { + Assert.ok( + urlEntry.base, + "Should have a base url for every URL defined on the top-level base object." + ); + } + } + } +} + +async function checkSearchConfigOverrideValidates( + schema, + searchConfigOverride +) { + let validator = new JsonSchema.Validator(schema); + + for (let entry of searchConfigOverride) { + // Records in Remote Settings contain additional properties independent of + // the schema. Hence, we don't want to validate their presence. + delete entry.schema; + delete entry.id; + delete entry.last_modified; + + let result = validator.validate(entry); + + let message = `Should validate ${entry.identifier ?? entry.telemetryId}`; + if (!result.valid) { + message += `:\n${JSON.stringify(result.errors, null, 2)}`; + } + Assert.ok(result.valid, message); + } +} + +add_task(async function test_search_config_validates_to_schema_v1() { + let selector = new SearchEngineSelectorOld(() => {}); + let searchConfig = await selector.getEngineConfiguration(); + + await checkSearchConfigValidates(searchConfigSchemaV1, searchConfig); +}); + +add_task(async function test_ui_schema_valid_v1() { + let uiSchema = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, "search-config-ui-schema.json") + ); + + await checkUISchemaValid(searchConfigSchemaV1, uiSchema); +}); + +add_task(async function test_search_config_override_validates_to_schema_v1() { + let selector = new SearchEngineSelectorOld(() => {}); + let searchConfigOverrides = await selector.getEngineConfigurationOverrides(); + let overrideSchema = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, "search-config-overrides-schema.json") + ); + + await checkSearchConfigOverrideValidates( + overrideSchema, + searchConfigOverrides + ); +}); + +add_task( + { skip_if: () => !SearchUtils.newSearchConfigEnabled }, + async function test_search_config_validates_to_schema() { + delete SearchUtils.newSearchConfigEnabled; + SearchUtils.newSearchConfigEnabled = true; + + let selector = new SearchEngineSelector(() => {}); + let searchConfig = await selector.getEngineConfiguration(); + + await checkSearchConfigValidates(searchConfigSchema, searchConfig); + } +); + +add_task( + { skip_if: () => !SearchUtils.newSearchConfigEnabled }, + async function test_ui_schema_valid() { + let uiSchema = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, "search-config-v2-ui-schema.json") + ); + + await checkUISchemaValid(searchConfigSchema, uiSchema); + } +); + +add_task( + { skip_if: () => !SearchUtils.newSearchConfigEnabled }, + async function test_search_config_override_validates_to_schema() { + let selector = new SearchEngineSelector(() => {}); + let searchConfigOverrides = + await selector.getEngineConfigurationOverrides(); + let overrideSchema = await IOUtils.readJSON( + PathUtils.join( + do_get_cwd().path, + "search-config-overrides-v2-schema.json" + ) + ); + + await checkSearchConfigOverrideValidates( + overrideSchema, + searchConfigOverrides + ); + } +); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchicons_validates.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchicons_validates.js new file mode 100644 index 0000000000..c830bb7ade --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchicons_validates.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let searchIconsSchema; + +add_setup(async function () { + searchIconsSchema = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, "search-config-icons-schema.json") + ); +}); + +add_task(async function test_ui_schema_valid() { + let uiSchema = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, "search-config-icons-ui-schema.json") + ); + + await checkUISchemaValid(searchIconsSchema, uiSchema); +}); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_selector_db_out_of_date.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_selector_db_out_of_date.js new file mode 100644 index 0000000000..9bd032c3b8 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_selector_db_out_of_date.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + RemoteSettingsWorker: + "resource://services-settings/RemoteSettingsWorker.sys.mjs", +}); + +do_get_profile(); + +add_task(async function test_selector_db_out_of_date() { + let searchConfig = RemoteSettings(SearchUtils.SETTINGS_KEY); + + // Do an initial get to pre-seed the database. + await searchConfig.get(); + + // Now clear the database and re-fill it. + let db = searchConfig.db; + await db.clear(); + let databaseEntries = await db.list(); + Assert.equal(databaseEntries.length, 0, "Should have cleared the database."); + + // Add a dummy record with an out-of-date last modified. + if (SearchUtils.newSearchConfigEnabled) { + await RemoteSettingsWorker._execute("_test_only_import", [ + "main", + SearchUtils.SETTINGS_KEY, + [ + { + id: "b70edfdd-1c3f-4b7b-ab55-38cb048636c0", + identifier: "outofdate", + recordType: "engine", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + last_modified: 1606227264000, + }, + ], + 1606227264000, + ]); + } else { + await RemoteSettingsWorker._execute("_test_only_import", [ + "main", + SearchUtils.SETTINGS_KEY, + [ + { + id: "b70edfdd-1c3f-4b7b-ab55-38cb048636c0", + default: "yes", + webExtension: { id: "outofdate@search.mozilla.org" }, + appliesTo: [{ included: { everywhere: true } }], + last_modified: 1606227264000, + }, + ], + 1606227264000, + ]); + } + + // Now load the configuration and check we get what we expect. + let engineSelector = SearchUtils.newSearchConfigEnabled + ? new SearchEngineSelector() + : new SearchEngineSelectorOld(); + + let result = await engineSelector.fetchEngineConfiguration({ + // Use the fallback default locale/regions to get a simple list. + locale: "default", + region: "default", + }); + + if (SearchUtils.newSearchConfigEnabled) { + Assert.deepEqual( + result.engines.map(e => e.identifier), + ["google", "ddg", "wikipedia"], + "Should have returned the correct data." + ); + } else { + Assert.deepEqual( + result.engines.map(e => e.webExtension.id), + [ + "google@search.mozilla.org", + "wikipedia@search.mozilla.org", + "ddg@search.mozilla.org", + ], + "Should have returned the correct data." + ); + } +}); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_wikipedia.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_wikipedia.js new file mode 100644 index 0000000000..54cf764830 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_wikipedia.js @@ -0,0 +1,171 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const testConfiguration = { + identifier: "wikipedia", + default: { + // Not default anywhere. + }, + available: { + excluded: [ + // Should be available everywhere. + ], + }, + details: [ + // Details generated below. + ], +}; + +/** + * Generates the expected details for the given locales and inserts + * them into the testConfiguration. + * + * @param {string[]} locales + * The locales for this details entry - which locales this variant of + * Wikipedia is expected to be deployed to. + * @param {string} [subDomainName] + * The expected sub domain name for this variant of Wikipedia. If not + * specified, defaults to the first item in the locales array. + * @param {string} [telemetrySuffix] + * The expected suffix used when this variant is reported via telemetry. If + * not specified, defaults to the first item in the array. If this is the + * empty string, then it "wikipedia" (i.e. no suffix) will be the expected + * value. + */ +function generateExpectedDetails(locales, subDomainName, telemetrySuffix) { + if (!subDomainName) { + subDomainName = locales[0]; + } + if (telemetrySuffix == undefined) { + telemetrySuffix = locales[0]; + } + testConfiguration.details.push({ + domain: `${subDomainName}.wikipedia.org`, + telemetryId: telemetrySuffix ? `wikipedia-${telemetrySuffix}` : "wikipedia", + aliases: ["@wikipedia"], + included: [{ locales }], + }); +} + +// This is an array of an array of arguments to be passed to generateExpectedDetails(). +// These are the locale, sub domain name and telemetry id suffix expectations for +// the test to check. +// Note that the expectations for en.wikipedia.com are generated in add_setup. +const LOCALES_INFO = [ + [["af"]], + [["an"]], + [["ar"]], + [["ast"]], + [["az"]], + [["be"]], + [["bg"]], + [["bn"]], + [["br"]], + [["bs"]], + [["ca", "ca-valencia"], "ca", "ca"], + [["cs"], "cs", "cz"], + [["cy"]], + [["da"]], + [["de"]], + [["dsb"]], + [["el"]], + [["eo"]], + [["cak", "es-AR", "es-CL", "es-ES", "es-MX", "trs"], "es", "es"], + [["et"]], + [["eu"]], + [["fa"]], + [["fi"]], + [["fr", "ff", "son"], "fr", "fr"], + [["fy-NL"], "fy", "fy-NL"], + [["ga-IE"], "ga", "ga-IE"], + [["gd"]], + [["gl"]], + [["gn"]], + [["gu-IN"], "gu", "gu"], + [["hi-IN"], "hi", "hi"], + [["he"]], + [["hr"]], + [["hsb"]], + [["hu"]], + [["hy-AM"], "hy", "hy"], + [["ia"]], + [["id"]], + [["is"]], + [["ja", "ja-JP-macos"], "ja", "ja"], + [["ka"]], + [["kab"]], + [["kk"]], + [["km"]], + [["kn"]], + [["ko"], "ko", "kr"], + [["it", "fur", "sc"], "it", "it"], + [["lij"]], + [["lo"]], + [["lt"]], + [["ltg"]], + [["lv"]], + [["mk"]], + [["mr"]], + [["ms"]], + [["my"]], + [["nb-NO"], "no", "NO"], + [["ne-NP"], "ne", "ne"], + [["nl"]], + [["nn-NO"], "nn", "NN"], + [["oc"]], + [["pa-IN"], "pa", "pa"], + [["pl", "szl"], "pl", "pl"], + [["pt-BR", "pt-PT"], "pt", "pt"], + [["rm"]], + [["ro"]], + [["ru"]], + [["si"]], + [["sk"]], + [["sl"]], + [["sq"]], + [["sr"]], + [["sv-SE"], "sv", "sv-SE"], + [["ta"]], + [["te"]], + [["th"]], + [["tl"]], + [["tr"]], + [["uk"]], + [["ur"]], + [["uz"]], + [["vi"]], + [["wo"]], + [["zh-CN"], "zh", "zh-CN"], + [["zh-TW"], "zh", "zh-TW"], +]; + +const test = new SearchConfigTest(testConfiguration); + +add_setup(async function () { + const allLocales = await test.getLocales(); + + // For the "en" version of Wikipedia, we ship it to all locales where other + // Wikipedias are not shipped. We form the list based on all-locales to avoid + // needing to update the test whenever all-locales is updated. + let enLocales = []; + for (let locale of allLocales) { + if (!LOCALES_INFO.find(d => d[0].includes(locale))) { + enLocales.push(locale); + } + } + + console.log("en.wikipedia.org expected locales are:", enLocales); + generateExpectedDetails(enLocales, "en", ""); + + for (let details of LOCALES_INFO) { + generateExpectedDetails(...details); + } + + await test.setup(); +}); + +add_task(async function test_searchConfig_wikipedia() { + await test.run(); +}); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_yahoojp.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_yahoojp.js new file mode 100644 index 0000000000..2b80dffd17 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_yahoojp.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const test = new SearchConfigTest({ + identifier: "yahoo-jp", + identifierExactMatch: true, + aliases: [], + default: { + // Not default anywhere. + }, + available: { + included: [ + { + locales: ["ja", "ja-JP-macos"], + }, + ], + }, + details: [ + { + included: [{}], + domain: "search.yahoo.co.jp", + telemetryId: "yahoo-jp", + searchUrlCode: "fr=mozff", + }, + ], +}); + +add_setup(async function () { + await test.setup(); +}); + +add_task(async function test_searchConfig_yahoojp() { + await test.run(); +}); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_yandex.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_yandex.js new file mode 100644 index 0000000000..6e2575b3c3 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_yandex.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const test = new SearchConfigTest({ + identifier: "yandex", + aliases: ["@\u044F\u043D\u0434\u0435\u043A\u0441", "@yandex"], + default: { + included: [ + { + regions: ["ru", "tr", "by", "kz"], + locales: ["ru", "tr", "be", "kk", "en-CA", "en-GB", "en-US"], + }, + ], + }, + available: { + included: [ + { + locales: ["az", "ru", "be", "kk", "tr"], + }, + { + regions: ["ru", "tr", "by", "kz"], + locales: ["en-CA", "en-GB", "en-US"], + }, + ], + }, + details: [ + { + included: [{ locales: ["az"] }], + domain: "yandex.az", + telemetryId: "yandex-az", + codes: { + searchbar: "clid=2186618", + keyword: "clid=2186621", + contextmenu: "clid=2186623", + homepage: "clid=2186617", + newtab: "clid=2186620", + }, + }, + { + included: [{ locales: { startsWith: ["en"] } }], + domain: "yandex.com", + telemetryId: "yandex-en", + codes: { + searchbar: "clid=2186618", + keyword: "clid=2186621", + contextmenu: "clid=2186623", + homepage: "clid=2186617", + newtab: "clid=2186620", + }, + }, + { + included: [{ locales: ["ru"] }], + domain: "yandex.ru", + telemetryId: "yandex-ru", + codes: { + searchbar: "clid=2186618", + keyword: "clid=2186621", + contextmenu: "clid=2186623", + homepage: "clid=2186617", + newtab: "clid=2186620", + }, + }, + { + included: [{ locales: ["be"] }], + domain: "yandex.by", + telemetryId: "yandex-by", + codes: { + searchbar: "clid=2186618", + keyword: "clid=2186621", + contextmenu: "clid=2186623", + homepage: "clid=2186617", + newtab: "clid=2186620", + }, + }, + { + included: [{ locales: ["kk"] }], + domain: "yandex.kz", + telemetryId: "yandex-kk", + codes: { + searchbar: "clid=2186618", + keyword: "clid=2186621", + contextmenu: "clid=2186623", + homepage: "clid=2186617", + newtab: "clid=2186620", + }, + }, + { + included: [{ locales: ["tr"] }], + domain: "yandex.com.tr", + telemetryId: "yandex-tr", + codes: { + searchbar: "clid=2186618", + keyword: "clid=2186621", + contextmenu: "clid=2186623", + homepage: "clid=2186617", + newtab: "clid=2186620", + }, + }, + ], +}); + +add_setup(async function () { + await test.setup(); +}); + +add_task(async function test_searchConfig_yandex() { + await test.run(); +}).skip(); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/xpcshell.toml b/toolkit/components/search/tests/xpcshell/searchconfigs/xpcshell.toml new file mode 100644 index 0000000000..8baff2a38d --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/xpcshell.toml @@ -0,0 +1,65 @@ +[DEFAULT] +firefox-appdir = "browser" +head = "head_searchconfig.js" +dupe-manifest = "" +support-files = ["../../../../../../browser/locales/all-locales"] +tags = "searchconfig remote-settings" +# These are extensive tests, we don't need to run them on asan/tsan. +# They are also skipped for mobile and Thunderbird as these are specifically +# testing the Firefox config at the moment. +skip-if = [ + "os == 'android'", + "appname == 'thunderbird'", + "asan", + "tsan", + "debug", + "os == 'win' && ccov", +] +# These tests do take a little longer on Linux ccov, so allow that here. +requesttimeoutfactor = 2 + +["test_amazon.js"] + +["test_baidu.js"] + +["test_bing.js"] + +["test_distributions.js"] + +["test_duckduckgo.js"] + +["test_ebay.js"] + +["test_ecosia.js"] + +["test_google.js"] + +["test_mailru.js"] + +["test_qwant.js"] + +["test_rakuten.js"] + +["test_searchconfig_validates.js"] +support-files = [ + "../../../schema/search-config-overrides-schema.json", + "../../../schema/search-config-overrides-v2-schema.json", + "../../../schema/search-config-schema.json", + "../../../schema/search-config-ui-schema.json", + "../../../schema/search-config-v2-schema.json", + "../../../schema/search-config-v2-ui-schema.json", +] + +["test_searchicons_validates.js"] +support-files = [ + "../../../schema/search-config-icons-schema.json", + "../../../schema/search-config-icons-ui-schema.json", +] + +["test_selector_db_out_of_date.js"] + +["test_wikipedia.js"] + +["test_yahoojp.js"] + +["test_yandex.js"] diff --git a/toolkit/components/search/tests/xpcshell/simple-engines/basic/manifest.json b/toolkit/components/search/tests/xpcshell/simple-engines/basic/manifest.json new file mode 100644 index 0000000000..b62eb9bb2b --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/simple-engines/basic/manifest.json @@ -0,0 +1,29 @@ +{ + "name": "basic", + "manifest_version": 2, + "version": "1.0", + "description": "basic", + "browser_specific_settings": { + "gecko": { + "id": "basic@search.mozilla.org" + } + }, + "hidden": true, + "chrome_settings_overrides": { + "search_provider": { + "name": "basic", + "search_url": "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB", + "params": [ + { + "name": "search", + "value": "{searchTerms}" + }, + { + "name": "sourceId", + "value": "Mozilla-search" + } + ], + "suggest_url": "https://ar.wikipedia.org/w/api.php?action=opensearch&search={searchTerms}" + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/simple-engines/engines.json b/toolkit/components/search/tests/xpcshell/simple-engines/engines.json new file mode 100644 index 0000000000..170ed8c4fe --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/simple-engines/engines.json @@ -0,0 +1,53 @@ +{ + "data": [ + { + "webExtension": { + "id": "basic@search.mozilla.org", + "name": "basic", + "search_url": "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB", + "params": [ + { + "name": "search", + "value": "{searchTerms}" + }, + { + "name": "sourceId", + "value": "Mozilla-search" + } + ], + "suggest_url": "https://ar.wikipedia.org/w/api.php?action=opensearch&search={searchTerms}" + }, + "telemetryId": "basic-telemetry", + "appliesTo": [ + { + "included": { "everywhere": true }, + "default": "yes" + } + ] + }, + { + "webExtension": { + "id": "simple@search.mozilla.org", + "name": "Simple Engine", + "search_url": "https://example.com", + "params": [ + { + "name": "sourceId", + "value": "Mozilla-search" + }, + { + "name": "search", + "value": "{searchTerms}" + } + ], + "suggest_url": "https://example.com?search={searchTerms}" + }, + "appliesTo": [ + { + "included": { "everywhere": true }, + "default": "no" + } + ] + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/simple-engines/search-config-v2.json b/toolkit/components/search/tests/xpcshell/simple-engines/search-config-v2.json new file mode 100644 index 0000000000..6b0541ab2c --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/simple-engines/search-config-v2.json @@ -0,0 +1,66 @@ +{ + "data": [ + { + "identifier": "basic", + "recordType": "engine", + "base": { + "name": "basic", + "urls": { + "search": { + "base": "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB", + "params": [ + { + "name": "sourceId", + "value": "Mozilla-search" + } + ], + "searchTermParamName": "search" + }, + "suggestions": { + "base": "https://ar.wikipedia.org/w/api.php?action=opensearch&search={searchTerms}" + } + } + }, + "variants": [ + { + "environment": { "allRegionsAndLocales": true }, + "telemetrySuffix": "telemetry" + } + ] + }, + { + "identifier": "simple", + "recordType": "engine", + "base": { + "name": "Simple Engine", + "urls": { + "search": { + "base": "https://example.com", + "params": [ + { + "name": "sourceId", + "value": "Mozilla-search" + } + ], + "searchTermParamName": "search" + }, + "suggestions": { "base": "https://example.com?search={searchTerms}" } + } + }, + "variants": [ + { + "environment": { "allRegionsAndLocales": true } + } + ] + }, + { + "recordType": "defaultEngines", + "globalDefault": "basic", + "specificDefaults": [] + }, + { + "recordType": "engineOrders", + "orders": [] + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/simple-engines/simple/manifest.json b/toolkit/components/search/tests/xpcshell/simple-engines/simple/manifest.json new file mode 100644 index 0000000000..67d2974753 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/simple-engines/simple/manifest.json @@ -0,0 +1,29 @@ +{ + "name": "Simple Engine", + "manifest_version": 2, + "version": "1.0", + "description": "Simple engine with a different name from the WebExtension id prefix", + "browser_specific_settings": { + "gecko": { + "id": "simple@search.mozilla.org" + } + }, + "hidden": true, + "chrome_settings_overrides": { + "search_provider": { + "name": "Simple Engine", + "search_url": "https://example.com", + "params": [ + { + "name": "sourceId", + "value": "Mozilla-search" + }, + { + "name": "search", + "value": "{searchTerms}" + } + ], + "suggest_url": "https://example.com?search={searchTerms}" + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/engines.json b/toolkit/components/search/tests/xpcshell/test-extensions/engines.json new file mode 100644 index 0000000000..0bedb236ab --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test-extensions/engines.json @@ -0,0 +1,128 @@ +{ + "data": [ + { + "webExtension": { + "id": "plainengine@search.mozilla.org", + "name": "Plain", + "search_url": "https://duckduckgo.com/", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "t", + "condition": "purpose", + "purpose": "contextmenu", + "value": "ffcm" + }, + { + "name": "t", + "condition": "purpose", + "purpose": "keyword", + "value": "ffab" + }, + { + "name": "t", + "condition": "purpose", + "purpose": "searchbar", + "value": "ffsb" + }, + { + "name": "t", + "condition": "purpose", + "purpose": "homepage", + "value": "ffhp" + }, + { + "name": "t", + "condition": "purpose", + "purpose": "newtab", + "value": "ffnt" + } + ], + "suggest_url": "https://ac.duckduckgo.com/ac/q={searchTerms}&type=list" + }, + "orderHint": 10000, + "appliesTo": [ + { + "included": { "everywhere": true }, + "default": "yes-if-no-other" + } + ] + }, + { + "webExtension": { + "id": "special-engine@search.mozilla.org", + "name": "Special", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "client", + "condition": "purpose", + "purpose": "keyword", + "value": "firefox-b-1-ab" + }, + { + "name": "client", + "condition": "purpose", + "purpose": "searchbar", + "value": "firefox-b-1" + } + ], + "suggest_url": "https://www.google.com/complete/search?client=firefox&q={searchTerms}" + }, + "orderHint": 7000, + "appliesTo": [ + { + "included": { "regions": ["tr"] }, + "default": "yes" + }, + { + "included": { "everywhere": true } + } + ] + }, + { + "webExtension": { + "id": "multilocale@search.mozilla.org", + "default_locale": "af", + "searchProvider": { + "an": { + "name": "Multilocale AN", + "description": "A enciclopedia Libre", + "search_url": "https://an.wikipedia.org/wiki/Especial:Mirar", + "suggest_url": "https://an.wikipedia.org/w/api.php" + }, + "af": { + "name": "Multilocale AF", + "description": "Wikipedia, die vrye ensiklopedie", + "search_url": "https://af.wikipedia.org/wiki/Spesiaal:Soek", + "suggest_url": "https://af.wikipedia.org/w/api.php" + } + } + }, + "orderHint": 6000, + "appliesTo": [ + { + "included": { "regions": ["an"] }, + "webExtension": { + "locales": ["an"] + }, + "default": "yes" + }, + { + "included": { "regions": ["af"] }, + "webExtension": { + "locales": ["af", "an"] + }, + "orderHint": 6500 + } + ] + } + ] +} diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/_locales/af/messages.json b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/_locales/af/messages.json new file mode 100644 index 0000000000..95e49f9bc5 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/_locales/af/messages.json @@ -0,0 +1,20 @@ +{ + "extensionName": { + "message": "Multilocale AF" + }, + "extensionDescription": { + "message": "Wikipedia, die vrye ensiklopedie" + }, + "url_lang": { + "message": "af" + }, + "searchUrl": { + "message": "https://af.wikipedia.org/wiki/Spesiaal:Soek" + }, + "suggestUrl": { + "message": "https://af.wikipedia.org/w/api.php" + }, + "extensionIcon": { + "message": "favicon-af.ico" + } +} diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/_locales/an/messages.json b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/_locales/an/messages.json new file mode 100644 index 0000000000..6222338596 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/_locales/an/messages.json @@ -0,0 +1,20 @@ +{ + "extensionName": { + "message": "Multilocale AN" + }, + "extensionDescription": { + "message": "A enciclopedia Libre" + }, + "url_lang": { + "message": "an" + }, + "searchUrl": { + "message": "https://an.wikipedia.org/wiki/Especial:Mirar" + }, + "suggestUrl": { + "message": "https://an.wikipedia.org/w/api.php" + }, + "extensionIcon": { + "message": "favicon-an.ico" + } +} diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/favicon-af.ico b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/favicon-af.ico Binary files differnew file mode 100644 index 0000000000..4314071e24 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/favicon-af.ico diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/favicon-an.ico b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/favicon-an.ico Binary files differnew file mode 100644 index 0000000000..dda80dfd88 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/favicon-an.ico diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/manifest.json b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/manifest.json new file mode 100644 index 0000000000..a117ffb0db --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test-extensions/multilocale/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "__MSG_extensionName__", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "multilocale@search.mozilla.org" + } + }, + "hidden": true, + "description": "__MSG_extensionDescription__", + "icons": { + "16": "__MSG_extensionIcon__" + }, + "default_locale": "af", + "chrome_settings_overrides": { + "search_provider": { + "name": "__MSG_extensionName__", + "search_url": "__MSG_searchUrl__", + "suggest_url": "__MSG_searchUrl__" + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/plainengine/favicon.ico b/toolkit/components/search/tests/xpcshell/test-extensions/plainengine/favicon.ico Binary files differnew file mode 100644 index 0000000000..dda80dfd88 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test-extensions/plainengine/favicon.ico diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/plainengine/manifest.json b/toolkit/components/search/tests/xpcshell/test-extensions/plainengine/manifest.json new file mode 100644 index 0000000000..cabb4c9f9a --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test-extensions/plainengine/manifest.json @@ -0,0 +1,58 @@ +{ + "name": "Plain", + "description": "Plain Engine", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "plainengine@search.mozilla.org" + } + }, + "hidden": true, + "icons": { + "16": "favicon.ico" + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "Plain", + "search_url": "https://duckduckgo.com/", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "t", + "condition": "purpose", + "purpose": "contextmenu", + "value": "ffcm" + }, + { + "name": "t", + "condition": "purpose", + "purpose": "keyword", + "value": "ffab" + }, + { + "name": "t", + "condition": "purpose", + "purpose": "searchbar", + "value": "ffsb" + }, + { + "name": "t", + "condition": "purpose", + "purpose": "homepage", + "value": "ffhp" + }, + { + "name": "t", + "condition": "purpose", + "purpose": "newtab", + "value": "ffnt" + } + ], + "suggest_url": "https://ac.duckduckgo.com/ac/q={searchTerms}&type=list" + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/special-engine/favicon.ico b/toolkit/components/search/tests/xpcshell/test-extensions/special-engine/favicon.ico Binary files differnew file mode 100644 index 0000000000..82339b3b1d --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test-extensions/special-engine/favicon.ico diff --git a/toolkit/components/search/tests/xpcshell/test-extensions/special-engine/manifest.json b/toolkit/components/search/tests/xpcshell/test-extensions/special-engine/manifest.json new file mode 100644 index 0000000000..1568c6ed55 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test-extensions/special-engine/manifest.json @@ -0,0 +1,40 @@ +{ + "name": "Special", + "description": "Special Engine", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "special-engine@search.mozilla.org" + } + }, + "hidden": true, + "icons": { + "16": "favicon.ico" + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "Special", + "search_url": "https://www.google.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + }, + { + "name": "client", + "condition": "purpose", + "purpose": "keyword", + "value": "firefox-b-1-ab" + }, + { + "name": "client", + "condition": "purpose", + "purpose": "searchbar", + "value": "firefox-b-1" + } + ], + "suggest_url": "https://www.google.com/complete/search?client=firefox&q={searchTerms}" + } + } +} diff --git a/toolkit/components/search/tests/xpcshell/test_SearchStaticData.js b/toolkit/components/search/tests/xpcshell/test_SearchStaticData.js new file mode 100644 index 0000000000..741953a1cf --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_SearchStaticData.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests the SearchStaticData module. + */ + +"use strict"; + +const { SearchStaticData } = ChromeUtils.importESModule( + "resource://gre/modules/SearchStaticData.sys.mjs" +); + +function run_test() { + Assert.ok( + SearchStaticData.getAlternateDomains("www.google.com").includes( + "www.google.fr" + ) + ); + Assert.ok( + SearchStaticData.getAlternateDomains("www.google.fr").includes( + "www.google.com" + ) + ); + Assert.ok( + SearchStaticData.getAlternateDomains("www.google.com").every(d => + d.startsWith("www.google.") + ) + ); + Assert.ok(!SearchStaticData.getAlternateDomains("google.com").length); + + // Test that methods from SearchStaticData module can be overwritten, + // needed for hotfixing. + let backup = SearchStaticData.getAlternateDomains; + SearchStaticData.getAlternateDomains = () => ["www.bing.fr"]; + Assert.deepEqual(SearchStaticData.getAlternateDomains("www.bing.com"), [ + "www.bing.fr", + ]); + SearchStaticData.getAlternateDomains = backup; +} diff --git a/toolkit/components/search/tests/xpcshell/test_appDefaultEngine.js b/toolkit/components/search/tests/xpcshell/test_appDefaultEngine.js new file mode 100644 index 0000000000..243c0cd967 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_appDefaultEngine.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test that appDefaultEngine property is set and switches correctly. + */ + +"use strict"; + +add_setup(async function () { + Region._setHomeRegion("an", false); + await AddonTestUtils.promiseStartupManager(); + await SearchTestUtils.useTestEngines("test-extensions"); +}); + +add_task(async function test_appDefaultEngine() { + await Promise.all([Services.search.init(), promiseAfterSettings()]); + Assert.equal( + Services.search.appDefaultEngine.name, + "Multilocale AN", + "Should have returned the correct app default engine" + ); +}); + +add_task(async function test_changeRegion() { + // Now change the region, and check we get the correct default according to + // the config file. + + // Note: the test could be done with changing regions or locales. The important + // part is that the default engine is changing across the switch, and that + // the engine is not the first one in the new sorted engines list. + await promiseSetHomeRegion("tr"); + + Assert.equal( + Services.search.appDefaultEngine.name, + // Very important this default is not the first one in the list (which is + // the next fallback if the config one can't be found). + "Special", + "Should have returned the correct engine for the new locale" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_async.js b/toolkit/components/search/tests/xpcshell/test_async.js new file mode 100644 index 0000000000..d66d402743 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_async.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_setup(async function () { + await AddonTestUtils.promiseStartupManager(); + await SearchTestUtils.useTestEngines("simple-engines"); + Services.fog.initializeFOG(); +}); + +add_task(async function test_async() { + Assert.ok(!Services.search.isInitialized); + + let aStatus = await Services.search.init(); + Assert.ok(Components.isSuccessCode(aStatus)); + Assert.ok(Services.search.isInitialized); + + // test engines from dir are not loaded. + let engines = await Services.search.getEngines(); + Assert.equal(engines.length, 2); + + // test jar engine is loaded ok. + let engine = Services.search.getEngineByName("basic"); + Assert.notEqual(engine, null); + Assert.ok(engine.isAppProvided, "Should be shown as an app-provided engine"); + + engine = Services.search.getEngineByName("Simple Engine"); + Assert.notEqual(engine, null); + Assert.ok(engine.isAppProvided, "Should be shown as an app-provided engine"); + + // Check if there is a value for startup_time + Assert.notEqual( + await Glean.searchService.startupTime.testGetValue(), + undefined, + "Should have a value stored in startup_time" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_config_engine_params.js b/toolkit/components/search/tests/xpcshell/test_config_engine_params.js new file mode 100644 index 0000000000..190ccbba2c --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_config_engine_params.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_setup(async function () { + await SearchTestUtils.useTestEngines("method-extensions"); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +}); + +add_task(async function test_get_extension() { + let engine = Services.search.getEngineByName("Get Engine"); + Assert.notEqual(engine, null, "Should have found an engine"); + + let url = engine.wrappedJSObject._getURLOfType(SearchUtils.URL_TYPE.SEARCH); + Assert.equal(url.method, "GET", "Search URLs method is GET"); + + let submission = engine.getSubmission("foo"); + Assert.equal( + submission.uri.spec, + "https://example.com/?config=1&search=foo", + "Search URLs should match" + ); + + let submissionSuggest = engine.getSubmission( + "bar", + SearchUtils.URL_TYPE.SUGGEST_JSON + ); + Assert.equal( + submissionSuggest.uri.spec, + "https://example.com/?config=1&suggest=bar", + "Suggest URLs should match" + ); +}); + +add_task(async function test_post_extension() { + let engine = Services.search.getEngineByName("Post Engine"); + Assert.ok(!!engine, "Should have found an engine"); + + let url = engine.wrappedJSObject._getURLOfType(SearchUtils.URL_TYPE.SEARCH); + Assert.equal(url.method, "POST", "Search URLs method is POST"); + + let submission = engine.getSubmission("foo"); + Assert.equal( + submission.uri.spec, + "https://example.com/", + "Search URLs should match" + ); + Assert.equal( + submission.postData.data.data, + "config=1&search=foo", + "Search postData should match" + ); + + let submissionSuggest = engine.getSubmission( + "bar", + SearchUtils.URL_TYPE.SUGGEST_JSON + ); + Assert.equal( + submissionSuggest.uri.spec, + "https://example.com/", + "Suggest URLs should match" + ); + Assert.equal( + submissionSuggest.postData.data.data, + "config=1&suggest=bar", + "Suggest postData should match" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_defaultEngine.js b/toolkit/components/search/tests/xpcshell/test_defaultEngine.js new file mode 100644 index 0000000000..9bab6b5423 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_defaultEngine.js @@ -0,0 +1,120 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test that defaultEngine property can be set and yields the proper events and\ + * behavior (search results) + */ + +"use strict"; + +let engine1; +let engine2; + +add_setup(async () => { + do_get_profile(); + Services.fog.initializeFOG(); + + useHttpServer(); + await AddonTestUtils.promiseStartupManager(); + + await Services.search.init(); + + engine1 = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engine.xml`, + }); + engine2 = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engine2.xml`, + }); +}); + +function promiseDefaultNotification() { + return SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.DEFAULT, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); +} + +add_task(async function test_defaultEngine() { + let promise = promiseDefaultNotification(); + Services.search.defaultEngine = engine1; + Assert.equal((await promise).wrappedJSObject, engine1); + Assert.equal(Services.search.defaultEngine.wrappedJSObject, engine1); + + await assertGleanDefaultEngine({ + normal: { + engineId: "other-Test search engine", + displayName: "Test search engine", + loadPath: "[http]localhost/test-search-engine.xml", + submissionUrl: "https://www.google.com/search?q=", + verified: "verified", + }, + }); + + promise = promiseDefaultNotification(); + Services.search.defaultEngine = engine2; + Assert.equal((await promise).wrappedJSObject, engine2); + Assert.equal(Services.search.defaultEngine.wrappedJSObject, engine2); + + await assertGleanDefaultEngine({ + normal: { + engineId: "other-A second test engine", + displayName: "A second test engine", + loadPath: "[http]localhost/a-second-test-engine.xml", + submissionUrl: "https://duckduckgo.com/?q=", + verified: "verified", + }, + }); + + promise = promiseDefaultNotification(); + Services.search.defaultEngine = engine1; + Assert.equal((await promise).wrappedJSObject, engine1); + Assert.equal(Services.search.defaultEngine.wrappedJSObject, engine1); + + await assertGleanDefaultEngine({ + normal: { + engineId: "other-Test search engine", + displayName: "Test search engine", + loadPath: "[http]localhost/test-search-engine.xml", + submissionUrl: "https://www.google.com/search?q=", + verified: "verified", + }, + }); +}); + +add_task(async function test_telemetry_empty_submission_url() { + await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}../opensearch/simple.xml`, + setAsDefaultPrivate: true, + }); + + await assertGleanDefaultEngine({ + normal: { + engineId: "other-simple", + displayName: "simple", + loadPath: "[http]localhost/simple.xml", + submissionUrl: "blank:", + verified: "verified", + }, + private: { + engineId: "", + displayName: "", + loadPath: "", + submissionUrl: "blank:", + verified: "", + }, + }); +}); + +add_task(async function test_switch_with_invalid_overriddenBy() { + engine1.wrappedJSObject.setAttr("overriddenBy", "random@id"); + + consoleAllowList.push( + "Test search engine had overriddenBy set, but no _overriddenData" + ); + + let promise = promiseDefaultNotification(); + Services.search.defaultEngine = engine2; + Assert.equal((await promise).wrappedJSObject, engine2); + Assert.equal(Services.search.defaultEngine.wrappedJSObject, engine2); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_defaultEngine_experiments.js b/toolkit/components/search/tests/xpcshell/test_defaultEngine_experiments.js new file mode 100644 index 0000000000..96fdf03d58 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_defaultEngine_experiments.js @@ -0,0 +1,480 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test that defaultEngine property can be set and yields the proper events and\ + * behavior (search results) + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", +}); + +let getVariableStub; + +let defaultGetVariable = name => { + if (name == "seperatePrivateDefaultUIEnabled") { + return true; + } + if (name == "seperatePrivateDefaultUrlbarResultEnabled") { + return false; + } + return undefined; +}; + +add_setup(async () => { + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + true + ); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); + + sinon.spy(NimbusFeatures.searchConfiguration, "onUpdate"); + sinon.stub(NimbusFeatures.searchConfiguration, "ready").resolves(); + getVariableStub = sinon.stub( + NimbusFeatures.searchConfiguration, + "getVariable" + ); + getVariableStub.callsFake(defaultGetVariable); + + do_get_profile(); + Services.fog.initializeFOG(); + + await SearchTestUtils.useTestEngines("data1"); + + await AddonTestUtils.promiseStartupManager(); + + let promiseSaved = promiseSaveSettingsData(); + await Services.search.init(); + await promiseSaved; +}); + +async function switchExperiment(newExperiment) { + let promiseReloaded = + SearchTestUtils.promiseSearchNotification("engines-reloaded"); + let promiseSaved = promiseSaveSettingsData(); + + // Stub getVariable to populate the cache with our expected data + getVariableStub.callsFake(name => { + if (name == "experiment") { + return newExperiment; + } + return defaultGetVariable(name); + }); + for (let call of NimbusFeatures.searchConfiguration.onUpdate.getCalls()) { + call.args[0](); + } + + await promiseReloaded; + await promiseSaved; +} + +function getSettingsAttribute(setting, isAppProvided) { + return Services.search.wrappedJSObject._settings.getVerifiedMetaDataAttribute( + setting, + isAppProvided + ); +} + +add_task(async function test_experiment_setting() { + Assert.equal( + Services.search.defaultEngine.name, + "engine1", + "Should have the application default engine as default" + ); + + // Start the experiment. + await switchExperiment("exp1"); + + Assert.equal( + Services.search.defaultEngine.name, + "engine2", + "Should have set the experiment engine as default" + ); + + // End the experiment. + await switchExperiment(""); + + Assert.equal( + Services.search.defaultEngine.name, + "engine1", + "Should have reset the default engine to the application default" + ); + Assert.equal( + getSettingsAttribute( + "defaultEngineId", + Services.search.defaultEngine.isAppProvided + ), + "", + "Should have kept the saved attribute as empty" + ); +}); + +add_task(async function test_experiment_setting_to_same_as_user() { + Services.search.defaultEngine = Services.search.getEngineByName("engine2"); + + Assert.equal( + Services.search.defaultEngine.name, + "engine2", + "Should have the user selected engine as default" + ); + Assert.equal( + getSettingsAttribute( + "defaultEngineId", + Services.search.defaultEngine.isAppProvided + ), + "engine2@search.mozilla.orgdefault" + ); + + // Start the experiment, ensure user default is maintained. + await switchExperiment("exp1"); + Assert.equal( + getSettingsAttribute( + "defaultEngineId", + Services.search.defaultEngine.isAppProvided + ), + "engine2@search.mozilla.orgdefault" + ); + + Assert.equal( + Services.search.appDefaultEngine.name, + "engine2", + "Should have set the experiment engine as default" + ); + Assert.equal( + Services.search.defaultEngine.name, + "engine2", + "Should have set the experiment engine as default" + ); + + // End the experiment. + await switchExperiment(""); + + Assert.equal( + Services.search.appDefaultEngine.name, + "engine1", + "Should have set the app default engine correctly" + ); + Assert.equal( + Services.search.defaultEngine.name, + "engine2", + "Should have kept the engine the same " + ); + Assert.equal( + getSettingsAttribute( + "defaultEngineId", + Services.search.defaultEngine.isAppProvided + ), + "engine2@search.mozilla.orgdefault", + "Should have kept the saved attribute as the user's preference" + ); +}); + +add_task(async function test_experiment_setting_user_changed_back_during() { + Services.search.defaultEngine = Services.search.getEngineByName("engine1"); + + Assert.equal( + Services.search.defaultEngine.name, + "engine1", + "Should have the application default engine as default" + ); + Assert.equal( + getSettingsAttribute( + "defaultEngineId", + Services.search.defaultEngine.isAppProvided + ), + "", + "Should have an empty settings attribute" + ); + + // Start the experiment. + await switchExperiment("exp1"); + + Assert.equal( + Services.search.defaultEngine.name, + "engine2", + "Should have set the experiment engine as default" + ); + Assert.equal( + getSettingsAttribute( + "defaultEngineId", + Services.search.defaultEngine.isAppProvided + ), + "", + "Should still have an empty settings attribute" + ); + + // User resets to the original default engine. + Services.search.defaultEngine = Services.search.getEngineByName("engine1"); + Assert.equal( + Services.search.defaultEngine.name, + "engine1", + "Should have the user selected engine as default" + ); + Assert.equal( + getSettingsAttribute( + "defaultEngineId", + Services.search.defaultEngine.isAppProvided + ), + "engine1@search.mozilla.orgdefault" + ); + + // Ending the experiment should keep the original default and reset the + // saved attribute. + await switchExperiment(""); + + Assert.equal( + Services.search.appDefaultEngine.name, + "engine1", + "Should have set the app default engine correctly" + ); + Assert.equal( + Services.search.defaultEngine.name, + "engine1", + "Should have kept the engine the same" + ); + Assert.equal( + getSettingsAttribute( + "defaultEngineId", + Services.search.defaultEngine.isAppProvided + ), + "", + "Should have reset the saved attribute to empty after the experiment ended" + ); +}); + +add_task(async function test_experiment_setting_user_changed_back_private() { + Services.search.defaultPrivateEngine = + Services.search.getEngineByName("engine1"); + + Assert.equal( + Services.search.defaultPrivateEngine.name, + "engine1", + "Should have the user selected engine as default" + ); + Assert.equal( + getSettingsAttribute( + "privateDefaultEngineId", + Services.search.defaultPrivateEngine.isAppProvided + ), + "", + "Should have an empty settings attribute" + ); + + // Start the experiment. + await switchExperiment("exp2"); + + Assert.equal( + Services.search.defaultPrivateEngine.name, + "exp2", + "Should have set the experiment engine as default" + ); + Assert.equal( + getSettingsAttribute( + "defaultEngineId", + Services.search.defaultEngine.isAppProvided + ), + "", + "Should still have an empty settings attribute" + ); + + // User resets to the original default engine. + Services.search.defaultPrivateEngine = + Services.search.getEngineByName("engine1"); + Assert.equal( + Services.search.defaultPrivateEngine.name, + "engine1", + "Should have the user selected engine as default" + ); + Assert.equal( + getSettingsAttribute( + "privateDefaultEngineId", + Services.search.defaultPrivateEngine.isAppProvided + ), + "engine1@search.mozilla.orgdefault" + ); + + // Ending the experiment should keep the original default and reset the + // saved attribute. + await switchExperiment(""); + + Assert.equal(Services.search.appPrivateDefaultEngine.name, "engine1"); + Assert.equal( + Services.search.defaultEngine.name, + "engine1", + "Should have kept the engine the same " + ); + Assert.equal( + getSettingsAttribute( + "privateDefaultEngineId", + Services.search.defaultPrivateEngine.isAppProvided + ), + "", + "Should have reset the saved attribute to empty after the experiment ended" + ); +}); + +add_task(async function test_experiment_setting_user_changed_to_other_during() { + Services.search.defaultEngine = Services.search.getEngineByName("engine1"); + + Assert.equal( + Services.search.defaultEngine.name, + "engine1", + "Should have the application default engine as default" + ); + Assert.equal( + getSettingsAttribute( + "defaultEngineId", + Services.search.defaultEngine.isAppProvided + ), + "", + "Should have an empty settings attribute" + ); + + // Start the experiment. + await switchExperiment("exp3"); + + Assert.equal( + Services.search.defaultEngine.name, + "exp3", + "Should have set the experiment engine as default" + ); + Assert.equal( + getSettingsAttribute( + "defaultEngineId", + Services.search.defaultEngine.isAppProvided + ), + "", + "Should still have an empty settings attribute" + ); + + // User changes to a different default engine + Services.search.defaultEngine = Services.search.getEngineByName("engine2"); + Assert.equal( + Services.search.defaultEngine.name, + "engine2", + "Should have the user selected engine as default" + ); + Assert.equal( + getSettingsAttribute( + "defaultEngineId", + Services.search.defaultEngine.isAppProvided + ), + "engine2@search.mozilla.orgdefault", + "Should have correctly set the user's default in settings" + ); + + // Ending the experiment should keep the original default and reset the + // saved attribute. + await switchExperiment(""); + + Assert.equal( + Services.search.appDefaultEngine.name, + "engine1", + "Should have set the app default engine correctly" + ); + Assert.equal( + Services.search.defaultEngine.name, + "engine2", + "Should have kept the user's choice of engine" + ); + Assert.equal( + getSettingsAttribute( + "defaultEngineId", + Services.search.defaultEngine.isAppProvided + ), + "engine2@search.mozilla.orgdefault", + "Should have kept the user's choice in settings" + ); +}); + +add_task(async function test_experiment_setting_user_hid_app_default_during() { + // Add all the test engines to be general search engines. This is important + // for the test, as the removed experiment engine needs to be a general search + // engine, and the first in the list (aided by the orderHint in + // data1/engines.json). + SearchUtils.GENERAL_SEARCH_ENGINE_IDS.add("engine1@search.mozilla.org"); + SearchUtils.GENERAL_SEARCH_ENGINE_IDS.add("engine2@search.mozilla.org"); + SearchUtils.GENERAL_SEARCH_ENGINE_IDS.add("exp2@search.mozilla.org"); + SearchUtils.GENERAL_SEARCH_ENGINE_IDS.add("exp3@search.mozilla.org"); + + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + false + ); + Services.search.defaultEngine = Services.search.getEngineByName("engine1"); + + Assert.equal( + Services.search.defaultEngine.name, + "engine1", + "Should have the application default engine as default" + ); + Assert.equal( + getSettingsAttribute( + "defaultEngineId", + Services.search.defaultEngine.isAppProvided + ), + "", + "Should have an empty settings attribute" + ); + + // Start the experiment. + await switchExperiment("exp3"); + + Assert.equal( + Services.search.defaultEngine.name, + "exp3", + "Should have set the experiment engine as default" + ); + Assert.equal( + getSettingsAttribute( + "defaultEngineId", + Services.search.defaultEngine.isAppProvided + ), + "", + "Should still have an empty settings attribute" + ); + + // User hides the original application engine + await Services.search.removeEngine( + Services.search.getEngineByName("engine1") + ); + Assert.equal( + Services.search.getEngineByName("engine1").hidden, + true, + "Should have hid the selected engine" + ); + + // Ending the experiment should keep the original default and reset the + // saved attribute. + await switchExperiment(""); + + Assert.equal( + Services.search.appDefaultEngine.name, + "engine1", + "Should have set the app default engine correctly" + ); + Assert.equal( + Services.search.defaultEngine.hidden, + false, + "Should not have set default engine to an engine that is hidden" + ); + Assert.equal( + Services.search.defaultEngine.name, + "engine2", + "Should have reset the user's engine to the next available engine" + ); + Assert.equal( + getSettingsAttribute( + "defaultEngineId", + Services.search.defaultEngine.isAppProvided + ), + "engine2@search.mozilla.orgdefault", + "Should have saved the choice in settings" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_defaultEngine_fallback.js b/toolkit/components/search/tests/xpcshell/test_defaultEngine_fallback.js new file mode 100644 index 0000000000..12cb6568e7 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_defaultEngine_fallback.js @@ -0,0 +1,406 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test is checking the fallbacks when an engine that is default is + * removed or hidden. + * + * The fallback procedure is: + * + * - Region/Locale default (if visible) + * - First visible engine + * - If no other visible engines, unhide the region/locale default and use it. + */ + +let appDefault; +let appPrivateDefault; + +add_setup(async function () { + useHttpServer(); + await SearchTestUtils.useTestEngines(); + + Services.prefs.setCharPref(SearchUtils.BROWSER_SEARCH_PREF + "region", "US"); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + true + ); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); + + SearchUtils.GENERAL_SEARCH_ENGINE_IDS = new Set([ + "engine-resourceicon@search.mozilla.org", + "engine-reordered@search.mozilla.org", + ]); + + await AddonTestUtils.promiseStartupManager(); + + appDefault = await Services.search.getDefault(); + appPrivateDefault = await Services.search.getDefaultPrivate(); +}); + +function getDefault(privateMode) { + return privateMode + ? Services.search.getDefaultPrivate() + : Services.search.getDefault(); +} + +function setDefault(privateMode, engine) { + return privateMode + ? Services.search.setDefaultPrivate( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ) + : Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); +} + +async function checkFallbackDefaultRegion(checkPrivate) { + let defaultEngine = checkPrivate ? appPrivateDefault : appDefault; + let expectedDefaultNotification = checkPrivate + ? SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE + : SearchUtils.MODIFIED_TYPE.DEFAULT; + Services.search.restoreDefaultEngines(); + + let otherEngine = Services.search.getEngineByName("engine-chromeicon"); + await setDefault(checkPrivate, otherEngine); + + Assert.notEqual( + otherEngine, + defaultEngine, + "Sanity check engines are different" + ); + + const observer = new SearchObserver( + [ + expectedDefaultNotification, + // For hiding (removing) the engine. + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.MODIFIED_TYPE.REMOVED, + ], + expectedDefaultNotification + ); + + await Services.search.removeEngine(otherEngine); + + let notified = await observer.promise; + + Assert.ok(otherEngine.hidden, "Should have hidden the removed engine"); + Assert.equal( + (await getDefault(checkPrivate)).name, + defaultEngine.name, + "Should have reverted the defaultEngine to the region default" + ); + Assert.equal( + notified.name, + defaultEngine.name, + "Should have notified the correct default engine" + ); +} + +add_task(async function test_default_fallback_to_region_default() { + await checkFallbackDefaultRegion(false); +}); + +add_task(async function test_default_private_fallback_to_region_default() { + await checkFallbackDefaultRegion(true); +}); + +async function checkFallbackFirstVisible(checkPrivate) { + let defaultEngine = checkPrivate ? appPrivateDefault : appDefault; + let expectedDefaultNotification = checkPrivate + ? SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE + : SearchUtils.MODIFIED_TYPE.DEFAULT; + Services.search.restoreDefaultEngines(); + + let otherEngine = Services.search.getEngineByName("engine-chromeicon"); + await setDefault(checkPrivate, otherEngine); + await Services.search.removeEngine(defaultEngine); + + Assert.notEqual( + otherEngine, + defaultEngine, + "Sanity check engines are different" + ); + + const observer = new SearchObserver( + checkPrivate + ? [ + expectedDefaultNotification, + // For hiding (removing) the engine. + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.MODIFIED_TYPE.REMOVED, + ] + : [ + expectedDefaultNotification, + // For hiding (removing) the engine. + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.MODIFIED_TYPE.REMOVED, + ], + expectedDefaultNotification + ); + + await Services.search.removeEngine(otherEngine); + + let notified = await observer.promise; + + Assert.equal( + (await getDefault(checkPrivate)).name, + "engine-resourceicon", + "Should have set the default engine to the first visible general engine" + ); + Assert.equal( + notified.name, + "engine-resourceicon", + "Should have notified the correct default general engine" + ); +} + +add_task(async function test_default_fallback_to_first_gen_visible() { + await checkFallbackFirstVisible(false); +}); + +add_task(async function test_default_private_fallback_to_first_gen_visible() { + await checkFallbackFirstVisible(true); +}); + +// Removing all visible engines affects both the default and private default +// engines. +add_task(async function test_default_fallback_when_no_others_visible() { + // Remove all but one of the visible engines. + let visibleEngines = await Services.search.getVisibleEngines(); + for (let i = 0; i < visibleEngines.length - 1; i++) { + await Services.search.removeEngine(visibleEngines[i]); + } + Assert.equal( + (await Services.search.getVisibleEngines()).length, + 1, + "Should only have one visible engine" + ); + + const observer = new SearchObserver( + [ + // Unhiding of the default engine. + SearchUtils.MODIFIED_TYPE.CHANGED, + // Change of the default. + SearchUtils.MODIFIED_TYPE.DEFAULT, + // Unhiding of the default private. + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE, + // Hiding the engine. + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.MODIFIED_TYPE.REMOVED, + ], + SearchUtils.MODIFIED_TYPE.DEFAULT + ); + + // Now remove the last engine, which should set the new default. + await Services.search.removeEngine(visibleEngines[visibleEngines.length - 1]); + + let notified = await observer.promise; + + Assert.equal( + (await getDefault(false)).name, + appDefault.name, + "Should fallback to the app default engine after removing all engines" + ); + Assert.equal( + (await getDefault(true)).name, + appPrivateDefault.name, + "Should fallback to the app default private engine after removing all engines" + ); + Assert.equal( + notified.name, + appDefault.name, + "Should have notified the correct default engine" + ); + Assert.ok( + !appPrivateDefault.hidden, + "Should have unhidden the app default private engine" + ); + Assert.equal( + (await Services.search.getVisibleEngines()).length, + 2, + "Should now have two engines visible" + ); +}); + +add_task(async function test_default_fallback_remove_default_no_visible() { + // Remove all but the default engine. + Services.search.defaultPrivateEngine = Services.search.defaultEngine; + let visibleEngines = await Services.search.getVisibleEngines(); + for (let engine of visibleEngines) { + if (engine.name != appDefault.name) { + await Services.search.removeEngine(engine); + } + } + Assert.equal( + (await Services.search.getVisibleEngines()).length, + 1, + "Should only have one visible engine" + ); + + const observer = new SearchObserver( + [ + // Unhiding of the default engine. + SearchUtils.MODIFIED_TYPE.CHANGED, + // Change of the default. + SearchUtils.MODIFIED_TYPE.DEFAULT, + SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE, + // Hiding the engine. + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.MODIFIED_TYPE.REMOVED, + ], + SearchUtils.MODIFIED_TYPE.DEFAULT + ); + + // Now remove the last engine, which should set the new default. + await Services.search.removeEngine(appDefault); + + let notified = await observer.promise; + + Assert.equal( + (await getDefault(false)).name, + "engine-resourceicon", + "Should fallback the default engine to the first general search engine" + ); + Assert.equal( + (await getDefault(true)).name, + "engine-resourceicon", + "Should fallback the default private engine to the first general search engine" + ); + Assert.equal( + notified.name, + "engine-resourceicon", + "Should have notified the correct default engine" + ); + Assert.ok( + !Services.search.getEngineByName("engine-resourceicon").hidden, + "Should have unhidden the new engine" + ); + Assert.equal( + (await Services.search.getVisibleEngines()).length, + 1, + "Should now have one engines visible" + ); +}); + +add_task( + async function test_default_fallback_remove_default_no_visible_or_general() { + // Reset. + Services.search.restoreDefaultEngines(); + Services.search.defaultEngine = Services.search.defaultPrivateEngine = + appPrivateDefault; + + // Remove all but the default engine. + let visibleEngines = await Services.search.getVisibleEngines(); + for (let engine of visibleEngines) { + if (engine.name != appPrivateDefault.name) { + await Services.search.removeEngine(engine); + } + } + Assert.deepEqual( + (await Services.search.getVisibleEngines()).map(e => e.name), + appPrivateDefault.name, + "Should only have one visible engine" + ); + + SearchUtils.GENERAL_SEARCH_ENGINE_IDS.clear(); + + const observer = new SearchObserver( + [ + // Unhiding of the default engine. + SearchUtils.MODIFIED_TYPE.CHANGED, + // Change of the default. + SearchUtils.MODIFIED_TYPE.DEFAULT, + SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE, + // Hiding the engine. + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.MODIFIED_TYPE.REMOVED, + ], + SearchUtils.MODIFIED_TYPE.DEFAULT + ); + + // Now remove the last engine, which should set the new default. + await Services.search.removeEngine(appPrivateDefault); + + let notified = await observer.promise; + + Assert.equal( + (await getDefault(false)).name, + "Test search engine", + "Should fallback to the first engine that isn't a general search engine" + ); + Assert.equal( + (await getDefault(true)).name, + "Test search engine", + "Should fallback the private engine to the first engine that isn't a general search engine" + ); + Assert.equal( + notified.name, + "Test search engine", + "Should have notified the correct default engine" + ); + Assert.ok( + !Services.search.getEngineByName("Test search engine").hidden, + "Should have unhidden the new engine" + ); + Assert.equal( + (await Services.search.getVisibleEngines()).length, + 1, + "Should now have one engines visible" + ); + } +); + +// Test the other remove engine route - for removing non-application provided +// engines. + +async function checkNonBuiltinFallback(checkPrivate) { + let defaultEngine = checkPrivate ? appPrivateDefault : appDefault; + let expectedDefaultNotification = checkPrivate + ? SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE + : SearchUtils.MODIFIED_TYPE.DEFAULT; + Services.search.restoreDefaultEngines(); + + let addedEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engine2.xml`, + }); + + await setDefault(checkPrivate, addedEngine); + + const observer = new SearchObserver( + [expectedDefaultNotification, SearchUtils.MODIFIED_TYPE.REMOVED], + expectedDefaultNotification + ); + + // Remove the current engine... + await Services.search.removeEngine(addedEngine); + + // ... and verify we've reverted to the normal default engine. + Assert.equal( + (await getDefault(checkPrivate)).name, + defaultEngine.name, + "Should revert to the app default engine" + ); + + let notified = await observer.promise; + Assert.equal( + notified.name, + defaultEngine.name, + "Should have notified the correct default engine" + ); +} + +add_task(async function test_default_fallback_non_builtin() { + await checkNonBuiltinFallback(false); +}); + +add_task(async function test_default_fallback_non_builtin_private() { + await checkNonBuiltinFallback(true); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_defaultPrivateEngine.js b/toolkit/components/search/tests/xpcshell/test_defaultPrivateEngine.js new file mode 100644 index 0000000000..053d91fe48 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_defaultPrivateEngine.js @@ -0,0 +1,593 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test that defaultEngine property can be set and yields the proper events and\ + * behavior (search results) + */ + +"use strict"; + +let engine1; +let engine2; +let appDefault; +let appPrivateDefault; + +add_setup(async () => { + do_get_profile(); + Services.fog.initializeFOG(); + + await SearchTestUtils.useTestEngines(); + + Services.prefs.setCharPref(SearchUtils.BROWSER_SEARCH_PREF + "region", "US"); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + true + ); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); + + useHttpServer("opensearch"); + await AddonTestUtils.promiseStartupManager(); + + await Services.search.init(); + + appDefault = Services.search.appDefaultEngine; + appPrivateDefault = Services.search.appPrivateDefaultEngine; + engine1 = Services.search.getEngineByName("engine-rel-searchform-purpose"); + engine2 = Services.search.getEngineByName("engine-chromeicon"); +}); + +add_task(async function test_defaultPrivateEngine() { + Assert.equal( + Services.search.defaultPrivateEngine, + appPrivateDefault, + "Should have the app private default as the default private engine" + ); + Assert.equal( + Services.search.defaultEngine, + appDefault, + "Should have the app default as the default engine" + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine", + displayName: "Test search engine", + loadPath: SearchUtils.newSearchConfigEnabled + ? "[app]engine@search.mozilla.org" + : "[addon]engine@search.mozilla.org", + submissionUrl: "https://www.google.com/search?q=", + verified: "default", + }, + private: { + engineId: "engine-pref", + displayName: "engine-pref", + loadPath: SearchUtils.newSearchConfigEnabled + ? "[app]engine-pref@search.mozilla.org" + : "[addon]engine-pref@search.mozilla.org", + submissionUrl: "https://www.google.com/search?q=", + verified: "default", + }, + }); + + let promise = promiseDefaultNotification("private"); + Services.search.defaultPrivateEngine = engine1; + Assert.equal( + await promise, + engine1, + "Should have notified setting the private engine to the new one" + ); + + Assert.equal( + Services.search.defaultPrivateEngine, + engine1, + "Should have set the private engine to the new one" + ); + Assert.equal( + Services.search.defaultEngine, + appDefault, + "Should not have changed the default engine" + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine", + displayName: "Test search engine", + loadPath: SearchUtils.newSearchConfigEnabled + ? "[app]engine@search.mozilla.org" + : "[addon]engine@search.mozilla.org", + submissionUrl: "https://www.google.com/search?q=", + verified: "default", + }, + private: { + engineId: "engine-rel-searchform-purpose", + displayName: "engine-rel-searchform-purpose", + loadPath: SearchUtils.newSearchConfigEnabled + ? "[app]engine-rel-searchform-purpose@search.mozilla.org" + : "[addon]engine-rel-searchform-purpose@search.mozilla.org", + submissionUrl: "https://www.google.com/search?q=&channel=sb", + verified: "default", + }, + }); + + promise = promiseDefaultNotification("private"); + await Services.search.setDefaultPrivate( + engine2, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + Assert.equal( + await promise, + engine2, + "Should have notified setting the private engine to the new one using async api" + ); + Assert.equal( + Services.search.defaultPrivateEngine, + engine2, + "Should have set the private engine to the new one using the async api" + ); + + // We use the names here as for some reason the getDefaultPrivate promise + // returns something which is an nsISearchEngine but doesn't compare + // exactly to what engine2 is. + Assert.equal( + (await Services.search.getDefaultPrivate()).name, + engine2.name, + "Should have got the correct private engine with the async api" + ); + Assert.equal( + Services.search.defaultEngine, + appDefault, + "Should not have changed the default engine" + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine", + displayName: "Test search engine", + loadPath: SearchUtils.newSearchConfigEnabled + ? "[app]engine@search.mozilla.org" + : "[addon]engine@search.mozilla.org", + submissionUrl: "https://www.google.com/search?q=", + verified: "default", + }, + private: { + engineId: "engine-chromeicon", + displayName: "engine-chromeicon", + loadPath: SearchUtils.newSearchConfigEnabled + ? "[app]engine-chromeicon@search.mozilla.org" + : "[addon]engine-chromeicon@search.mozilla.org", + submissionUrl: "https://www.google.com/search?q=", + verified: "default", + }, + }); + + promise = promiseDefaultNotification("private"); + await Services.search.setDefaultPrivate( + engine1, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + Assert.equal( + await promise, + engine1, + "Should have notified reverting the private engine to the selected one using async api" + ); + Assert.equal( + Services.search.defaultPrivateEngine, + engine1, + "Should have reverted the private engine to the selected one using the async api" + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine", + }, + private: { + engineId: "engine-rel-searchform-purpose", + }, + }); + + engine1.hidden = true; + Assert.equal( + Services.search.defaultPrivateEngine, + appPrivateDefault, + "Should reset to the app default private engine when hiding the default" + ); + Assert.equal( + Services.search.defaultEngine, + appDefault, + "Should not have changed the default engine" + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine", + }, + private: { + engineId: "engine-pref", + }, + }); + + engine1.hidden = false; + Services.search.defaultEngine = engine1; + Assert.equal( + Services.search.defaultPrivateEngine, + appPrivateDefault, + "Setting the default engine should not affect the private default" + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine-rel-searchform-purpose", + }, + private: { + engineId: "engine-pref", + }, + }); + + Services.search.defaultEngine = appDefault; +}); + +add_task(async function test_telemetry_private_empty_submission_url() { + await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}simple.xml`, + setAsDefaultPrivate: true, + }); + + await assertGleanDefaultEngine({ + normal: { + engineId: appDefault.telemetryId, + }, + private: { + engineId: "other-simple", + displayName: "simple", + loadPath: "[http]localhost/simple.xml", + submissionUrl: "blank:", + verified: "verified", + }, + }); + + Services.search.defaultEngine = appDefault; +}); + +add_task(async function test_defaultPrivateEngine_turned_off() { + Services.search.defaultEngine = appDefault; + Services.search.defaultPrivateEngine = engine1; + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine", + }, + private: { + engineId: "engine-rel-searchform-purpose", + }, + }); + + let promise = promiseDefaultNotification("private"); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + false + ); + Assert.equal( + await promise, + appDefault, + "Should have notified setting the first engine correctly." + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine", + }, + private: { + engineId: "", + }, + }); + + promise = promiseDefaultNotification("normal"); + let privatePromise = promiseDefaultNotification("private"); + Services.search.defaultEngine = engine1; + Assert.equal( + await promise, + engine1, + "Should have notified setting the first engine correctly." + ); + Assert.equal( + await privatePromise, + engine1, + "Should have notified setting of the private engine as well." + ); + Assert.equal( + Services.search.defaultPrivateEngine, + engine1, + "Should be set to the first engine correctly" + ); + Assert.equal( + Services.search.defaultEngine, + engine1, + "Should keep the default engine in sync with the pref off" + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine-rel-searchform-purpose", + }, + private: { + engineId: "", + }, + }); + + promise = promiseDefaultNotification("private"); + Services.search.defaultPrivateEngine = engine2; + Assert.equal( + await promise, + engine2, + "Should have notified setting the second engine correctly." + ); + Assert.equal( + Services.search.defaultPrivateEngine, + engine2, + "Should be set to the second engine correctly" + ); + Assert.equal( + Services.search.defaultEngine, + engine1, + "Should not change the normal mode default engine" + ); + Assert.equal( + Services.prefs.getBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + false + ), + true, + "Should have set the separate private default pref to true" + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine-rel-searchform-purpose", + }, + private: { + engineId: "engine-chromeicon", + }, + }); + + promise = promiseDefaultNotification("private"); + Services.search.defaultPrivateEngine = engine1; + Assert.equal( + await promise, + engine1, + "Should have notified resetting to the first engine again" + ); + Assert.equal( + Services.search.defaultPrivateEngine, + engine1, + "Should be reset to the first engine again" + ); + Assert.equal( + Services.search.defaultEngine, + engine1, + "Should keep the default engine in sync with the pref off" + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine-rel-searchform-purpose", + }, + private: { + engineId: "engine-rel-searchform-purpose", + }, + }); +}); + +add_task(async function test_defaultPrivateEngine_ui_turned_off() { + engine1.hidden = false; + engine2.hidden = false; + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); + + Services.search.defaultEngine = engine2; + Services.search.defaultPrivateEngine = engine1; + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine-chromeicon", + }, + private: { + engineId: "engine-rel-searchform-purpose", + }, + }); + + let promise = promiseDefaultNotification("private"); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + false + ); + Assert.equal( + await promise, + engine2, + "Should have notified for resetting of the private pref." + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine-chromeicon", + }, + private: { + engineId: "", + }, + }); + + promise = promiseDefaultNotification("normal"); + Services.search.defaultPrivateEngine = engine1; + Assert.equal( + await promise, + engine1, + "Should have notified setting the first engine correctly." + ); + Assert.equal( + Services.search.defaultPrivateEngine, + engine1, + "Should be set to the first engine correctly" + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine-rel-searchform-purpose", + }, + private: { + engineId: "", + }, + }); +}); + +add_task(async function test_defaultPrivateEngine_same_engine_toggle_pref() { + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + true + ); + + // Set the normal and private engines to be the same + Services.search.defaultEngine = engine2; + Services.search.defaultPrivateEngine = engine2; + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine-chromeicon", + }, + private: { + engineId: "engine-chromeicon", + }, + }); + + // Disable pref + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + false + ); + Assert.equal( + Services.search.defaultPrivateEngine, + engine2, + "Should not change the default private engine" + ); + Assert.equal( + Services.search.defaultEngine, + engine2, + "Should not change the default engine" + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine-chromeicon", + }, + private: { + engineId: "", + }, + }); + + // Re-enable pref + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); + Assert.equal( + Services.search.defaultPrivateEngine, + engine2, + "Should not change the default private engine" + ); + Assert.equal( + Services.search.defaultEngine, + engine2, + "Should not change the default engine" + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine-chromeicon", + }, + private: { + engineId: "engine-chromeicon", + }, + }); +}); + +add_task(async function test_defaultPrivateEngine_same_engine_toggle_ui_pref() { + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + true + ); + + // Set the normal and private engines to be the same + Services.search.defaultEngine = engine2; + Services.search.defaultPrivateEngine = engine2; + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine-chromeicon", + }, + private: { + engineId: "engine-chromeicon", + }, + }); + + // Disable UI pref + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + false + ); + Assert.equal( + Services.search.defaultPrivateEngine, + engine2, + "Should not change the default private engine" + ); + Assert.equal( + Services.search.defaultEngine, + engine2, + "Should not change the default engine" + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine-chromeicon", + }, + private: { + engineId: "", + }, + }); + + // Re-enable UI pref + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + true + ); + Assert.equal( + Services.search.defaultPrivateEngine, + engine2, + "Should not change the default private engine" + ); + Assert.equal( + Services.search.defaultEngine, + engine2, + "Should not change the default engine" + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine-chromeicon", + }, + private: { + engineId: "engine-chromeicon", + }, + }); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_alias.js b/toolkit/components/search/tests/xpcshell/test_engine_alias.js new file mode 100644 index 0000000000..2b0aa41850 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_alias.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const NAME = "Test Alias Engine"; + +add_setup(async function () { + await AddonTestUtils.promiseStartupManager(); + let settingsFileWritten = promiseAfterSettings(); + await Services.search.init(); + await settingsFileWritten; +}); + +add_task(async function upgrade_with_configuration_change_test() { + let settingsFileWritten = promiseAfterSettings(); + await SearchTestUtils.installSearchExtension({ + name: NAME, + keyword: "testalias", + }); + await settingsFileWritten; + + let engine = await Services.search.getEngineByAlias("testalias"); + Assert.equal(engine?.name, NAME, "Engine can be fetched by alias"); + + // Restart the search service but not the AddonManager, we will + // load the engines from settings. + Services.search.wrappedJSObject.reset(); + await Services.search.init(); + + engine = await Services.search.getEngineByAlias("testalias"); + Assert.equal(engine?.name, NAME, "Engine can be fetched by alias"); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_ids.js b/toolkit/components/search/tests/xpcshell/test_engine_ids.js new file mode 100644 index 0000000000..cef6a17c92 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_ids.js @@ -0,0 +1,158 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests Search Engine IDs are created correctly. + */ + +"use strict"; + +const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" +); + +const CONFIG = [ + { + webExtension: { + id: "engine@search.mozilla.org", + name: "Test search engine", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "channel", + condition: "purpose", + purpose: "contextmenu", + value: "rcs", + }, + { + name: "channel", + condition: "purpose", + purpose: "keyword", + value: "fflb", + }, + ], + suggest_url: + "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}", + }, + appliesTo: [ + { + included: { everywhere: true }, + default: "yes", + }, + ], + }, +]; + +add_setup(async function () { + useHttpServer("opensearch"); + await SearchTestUtils.useTestEngines("data", null, CONFIG); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +}); + +add_task(async function test_add_on_engine_id() { + let addOnEngine = Services.search.defaultEngine; + + Assert.equal( + addOnEngine.name, + "Test search engine", + "Should have installed the Test search engine as default." + ); + Assert.ok(addOnEngine.id, "The Addon Search Engine should have an id."); + Assert.equal( + addOnEngine.id, + "engine@search.mozilla.orgdefault", + "The Addon Search Engine id should be the webextension id + the locale." + ); +}); + +add_task(async function test_user_engine_id() { + let promiseEngineAdded = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.ADDED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + + await Services.search.addUserEngine( + "user", + "https://example.com/user?q={searchTerms}", + "u" + ); + + await promiseEngineAdded; + let userEngine = Services.search.getEngineByName("user"); + + Assert.ok(userEngine, "Should have installed the User Search Engine."); + Assert.ok(userEngine.id, "The User Search Engine should have an id."); + Assert.equal( + userEngine.id.length, + 36, + "The User Search Engine id should be a 36 character uuid." + ); +}); + +add_task(async function test_open_search_engine_id() { + let openSearchEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: gDataUrl + "simple.xml", + }); + + Assert.ok(openSearchEngine, "Should have installed the Open Search Engine."); + Assert.ok(openSearchEngine.id, "The Open Search Engine should have an id."); + Assert.equal( + openSearchEngine.id.length, + 36, + "The Open Search Engine id should be a 36 character uuid." + ); +}); + +add_task(async function test_enterprise_policy_engine_id() { + await setupPolicyEngineWithJson({ + policies: { + SearchEngines: { + Add: [ + { + Name: "policy", + Description: "Test policy engine", + IconURL: "data:image/gif;base64,R0lGODl", + Alias: "p", + URLTemplate: "https://example.com?q={searchTerms}", + SuggestURLTemplate: "https://example.com/suggest/?q={searchTerms}", + }, + ], + }, + }, + }); + + let policyEngine = Services.search.getEngineByName("policy"); + + Assert.ok(policyEngine, "Should have installed the Policy Engine."); + Assert.ok(policyEngine.id, "The Policy Engine should have an id."); + Assert.equal( + policyEngine.id, + "policy-policy", + "The Policy Engine id should be 'policy-' + 'the name of the policy engine'." + ); +}); + +/** + * Loads a new enterprise policy, and re-initialise the search service + * with the new policy. Also waits for the search service to write the settings + * file to disk. + * + * @param {object} policy + * The enterprise policy to use. + */ +async function setupPolicyEngineWithJson(policy) { + Services.search.wrappedJSObject.reset(); + + await EnterprisePolicyTesting.setupPolicyEngineWithJson(policy); + + let settingsWritten = SearchTestUtils.promiseSearchNotification( + "write-settings-to-disk-complete" + ); + await Services.search.init(); + await settingsWritten; +} diff --git a/toolkit/components/search/tests/xpcshell/test_engine_multiple_alias.js b/toolkit/components/search/tests/xpcshell/test_engine_multiple_alias.js new file mode 100644 index 0000000000..5b3138ced1 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_multiple_alias.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const NAME = "Test Alias Engine"; + +add_setup(async function () { + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +}); + +add_task(async function upgrade_with_configuration_change_test() { + let settingsFileWritten = promiseAfterSettings(); + await SearchTestUtils.installSearchExtension({ + name: NAME, + keyword: [" test", "alias "], + }); + await settingsFileWritten; + + let engine = await Services.search.getEngineByAlias("test"); + Assert.equal(engine?.name, NAME, "Can be fetched by either alias"); + engine = await Services.search.getEngineByAlias("alias"); + Assert.equal(engine?.name, NAME, "Can be fetched by either alias"); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_old_selector.js b/toolkit/components/search/tests/xpcshell/test_engine_old_selector.js new file mode 100644 index 0000000000..12344021d2 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_old_selector.js @@ -0,0 +1,242 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SearchEngineSelectorOld: + "resource://gre/modules/SearchEngineSelectorOld.sys.mjs", +}); + +const TEST_CONFIG = [ + { + engineName: "aol", + orderHint: 500, + webExtension: { + locales: ["default"], + }, + appliesTo: [ + { + included: { everywhere: true }, + }, + { + included: { regions: ["us"] }, + webExtension: { + locales: ["baz-$USER_LOCALE"], + }, + telemetryId: "foo-$USER_LOCALE", + }, + { + included: { regions: ["fr"] }, + webExtension: { + locales: ["region-$USER_REGION"], + }, + telemetryId: "bar-$USER_REGION", + }, + { + included: { regions: ["be"] }, + webExtension: { + locales: ["$USER_LOCALE"], + }, + telemetryId: "$USER_LOCALE", + }, + { + included: { regions: ["au"] }, + webExtension: { + locales: ["$USER_REGION"], + }, + telemetryId: "$USER_REGION", + }, + ], + }, + { + engineName: "lycos", + orderHint: 1000, + default: "yes", + appliesTo: [ + { + included: { everywhere: true }, + excluded: { locales: { matches: ["zh-CN"] } }, + }, + ], + }, + { + engineName: "altavista", + orderHint: 2000, + defaultPrivate: "yes", + appliesTo: [ + { + included: { locales: { matches: ["en-US"] } }, + }, + { + included: { regions: ["default"] }, + }, + ], + }, + { + engineName: "excite", + default: "yes-if-no-other", + appliesTo: [ + { + included: { everywhere: true }, + excluded: { regions: ["us"] }, + }, + { + included: { everywhere: true }, + experiment: "acohortid", + }, + ], + }, + { + engineName: "askjeeves", + }, +]; + +const engineSelector = new SearchEngineSelectorOld(); + +add_setup(async function () { + const settings = await RemoteSettings(SearchUtils.OLD_SETTINGS_KEY); + sinon.stub(settings, "get").returns(TEST_CONFIG); +}); + +add_task(async function test_engine_selector() { + let { engines, privateDefault } = + await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "us", + }); + Assert.equal( + privateDefault.engineName, + "altavista", + "Should set altavista as privateDefault" + ); + let names = engines.map(obj => obj.engineName); + Assert.deepEqual(names, ["lycos", "altavista", "aol"], "Correct order"); + Assert.equal( + engines[2].webExtension.locale, + "baz-en-US", + "Subsequent matches in applies to can override default" + ); + + ({ engines, privateDefault } = await engineSelector.fetchEngineConfiguration({ + locale: "zh-CN", + region: "kz", + })); + Assert.equal(engines.length, 2, "Correct engines are returns"); + Assert.equal(privateDefault, null, "There should be no privateDefault"); + names = engines.map(obj => obj.engineName); + Assert.deepEqual( + names, + ["excite", "aol"], + "The engines should be in the correct order" + ); + + ({ engines, privateDefault } = await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "us", + experiment: "acohortid", + })); + Assert.deepEqual( + engines.map(obj => obj.engineName), + ["lycos", "altavista", "aol", "excite"], + "Engines are in the correct order and include the experiment engine" + ); + + ({ engines, privateDefault } = await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "default", + experiment: "acohortid", + })); + Assert.deepEqual( + engines.map(obj => obj.engineName), + ["lycos", "altavista", "aol", "excite"], + "The engines should be in the correct order" + ); + Assert.equal( + privateDefault.engineName, + "altavista", + "Should set altavista as privateDefault" + ); +}); + +add_task(async function test_locale_region_replacement() { + let { engines } = await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "us", + }); + let engine = engines.find(e => e.engineName == "aol"); + Assert.equal( + engine.webExtension.locale, + "baz-en-US", + "The locale is correctly inserted into the locale field" + ); + Assert.equal( + engine.telemetryId, + "foo-en-US", + "The locale is correctly inserted into the telemetryId" + ); + + ({ engines } = await engineSelector.fetchEngineConfiguration({ + locale: "it", + region: "us", + })); + engine = engines.find(e => e.engineName == "aol"); + + Assert.equal( + engines.find(e => e.engineName == "aol").webExtension.locale, + "baz-it", + "The locale is correctly inserted into the locale field" + ); + Assert.equal( + engine.telemetryId, + "foo-it", + "The locale is correctly inserted into the telemetryId" + ); + + ({ engines } = await engineSelector.fetchEngineConfiguration({ + locale: "en-CA", + region: "fr", + })); + engine = engines.find(e => e.engineName == "aol"); + Assert.equal( + engine.webExtension.locale, + "region-fr", + "The region is correctly inserted into the locale field" + ); + Assert.equal( + engine.telemetryId, + "bar-fr", + "The region is correctly inserted into the telemetryId" + ); + + ({ engines } = await engineSelector.fetchEngineConfiguration({ + locale: "fy-NL", + region: "be", + })); + engine = engines.find(e => e.engineName == "aol"); + Assert.equal( + engine.webExtension.locale, + "fy-NL", + "The locale is correctly inserted into the locale field" + ); + Assert.equal( + engine.telemetryId, + "fy-NL", + "The locale is correctly inserted into the telemetryId" + ); + ({ engines } = await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "au", + })); + engine = engines.find(e => e.engineName == "aol"); + Assert.equal( + engine.webExtension.locale, + "au", + "The region is correctly inserted into the locale field" + ); + Assert.equal( + engine.telemetryId, + "au", + "The region is correctly inserted into the telemetryId" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_old_selector_application.js b/toolkit/components/search/tests/xpcshell/test_engine_old_selector_application.js new file mode 100644 index 0000000000..3270bdb55e --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_old_selector_application.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SearchEngineSelectorOld: + "resource://gre/modules/SearchEngineSelectorOld.sys.mjs", +}); + +const TEST_CONFIG = [ + { + webExtension: { + id: "aol@example.com", + }, + appliesTo: [ + { + included: { everywhere: true }, + }, + ], + default: "yes-if-no-other", + }, + { + webExtension: { + id: "lycos@example.com", + }, + appliesTo: [ + { + included: { everywhere: true }, + application: { + channel: ["nightly"], + }, + }, + ], + default: "yes", + }, + { + webExtension: { + id: "altavista@example.com", + }, + appliesTo: [ + { + included: { everywhere: true }, + application: { + channel: ["nightly", "esr"], + }, + }, + ], + }, + { + webExtension: { + id: "excite@example.com", + }, + appliesTo: [ + { + included: { everywhere: true }, + }, + { + included: { everywhere: true }, + application: { + channel: ["release"], + }, + default: "yes", + }, + ], + }, +]; + +const expectedEnginesPerChannel = { + default: ["aol@example.com", "excite@example.com"], + nightly: [ + "lycos@example.com", + "aol@example.com", + "altavista@example.com", + "excite@example.com", + ], + beta: ["aol@example.com", "excite@example.com"], + release: ["excite@example.com", "aol@example.com"], + esr: ["aol@example.com", "altavista@example.com", "excite@example.com"], +}; + +const expectedDefaultEngine = { + default: "aol@example.com", + nightly: "lycos@example.com", + beta: "aol@example.com", + release: "excite@example.com", + esr: "aol@example.com", +}; + +const engineSelector = new SearchEngineSelectorOld(); + +add_task(async function test_engine_selector_channels() { + const settings = await RemoteSettings(SearchUtils.OLD_SETTINGS_KEY); + sinon.stub(settings, "get").returns(TEST_CONFIG); + + for (let [channel, expected] of Object.entries(expectedEnginesPerChannel)) { + const { engines } = await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "us", + channel, + }); + + const engineIds = engines.map(obj => obj.webExtension.id); + Assert.deepEqual( + engineIds, + expected, + `Should have the expected engines for channel "${channel}"` + ); + + Assert.equal( + engineIds[0], + expectedDefaultEngine[channel], + `Should have the correct default for channel "${channel}"` + ); + } +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_old_selector_application_distribution.js b/toolkit/components/search/tests/xpcshell/test_engine_old_selector_application_distribution.js new file mode 100644 index 0000000000..01252740a5 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_old_selector_application_distribution.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SearchEngineSelectorOld: + "resource://gre/modules/SearchEngineSelectorOld.sys.mjs", +}); + +const CONFIG = [ + { + webExtension: { + id: "aol@example.com", + }, + appliesTo: [ + { + included: { everywhere: true }, + }, + ], + default: "yes-if-no-other", + }, + { + webExtension: { + id: "excite@example.com", + }, + appliesTo: [ + { + included: { everywhere: true }, + // Test with a application/distributions section present but an + // empty list. + application: { + distributions: [], + }, + }, + ], + }, + { + webExtension: { + id: "lycos@example.com", + }, + appliesTo: [ + { + included: { everywhere: true }, + application: { + distributions: ["cake"], + }, + }, + ], + default: "yes", + }, + { + webExtension: { + id: "altavista@example.com", + }, + appliesTo: [ + { + included: { everywhere: true }, + application: { + excludedDistributions: ["apples"], + }, + }, + ], + }, +]; + +const engineSelector = new SearchEngineSelectorOld(); +add_setup(async function () { + Services.prefs.setBoolPref("browser.search.newSearchConfig.enabled", false); + await SearchTestUtils.useTestEngines("data", null, CONFIG); + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_no_distribution_preference() { + let { engines } = await engineSelector.fetchEngineConfiguration({ + locale: "default", + region: "default", + channel: "", + distroID: "", + }); + const engineIds = engines.map(obj => obj.webExtension.id); + Assert.deepEqual( + engineIds, + ["aol@example.com", "excite@example.com", "altavista@example.com"], + `Should have the expected engines for a normal build.` + ); +}); + +add_task(async function test_distribution_included() { + let { engines } = await engineSelector.fetchEngineConfiguration({ + locale: "default", + region: "default", + channel: "", + distroID: "cake", + }); + const engineIds = engines.map(obj => obj.webExtension.id); + Assert.deepEqual( + engineIds, + [ + "lycos@example.com", + "aol@example.com", + "excite@example.com", + "altavista@example.com", + ], + `Should have the expected engines for the "cake" distribution.` + ); +}); + +add_task(async function test_distribution_excluded() { + let { engines } = await engineSelector.fetchEngineConfiguration({ + locale: "default", + region: "default", + channel: "", + distroID: "apples", + }); + const engineIds = engines.map(obj => obj.webExtension.id); + Assert.deepEqual( + engineIds, + ["aol@example.com", "excite@example.com"], + `Should have the expected engines for the "apples" distribution.` + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_old_selector_application_name.js b/toolkit/components/search/tests/xpcshell/test_engine_old_selector_application_name.js new file mode 100644 index 0000000000..f38198aca6 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_old_selector_application_name.js @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SearchEngineSelectorOld: + "resource://gre/modules/SearchEngineSelectorOld.sys.mjs", +}); + +const CONFIG = [ + { + webExtension: { + id: "aol@example.com", + }, + appliesTo: [ + { + included: { everywhere: true }, + }, + ], + default: "yes-if-no-other", + }, + { + webExtension: { + id: "lycos@example.com", + }, + appliesTo: [ + { + included: { everywhere: true }, + application: { + name: ["firefox"], + }, + }, + ], + default: "yes", + }, + { + webExtension: { + id: "altavista@example.com", + }, + appliesTo: [ + { + included: { everywhere: true }, + application: { + name: ["fenix"], + }, + }, + ], + }, + { + webExtension: { + id: "excite@example.com", + }, + appliesTo: [ + { + included: { everywhere: true }, + application: { + name: ["firefox"], + minVersion: "10", + maxVersion: "30", + }, + default: "yes", + }, + ], + }, +]; + +const engineSelector = new SearchEngineSelectorOld(); + +const tests = [ + { + name: "Firefox", + version: "1", + expected: ["lycos@example.com", "aol@example.com"], + }, + { + name: "Firefox", + version: "20", + expected: ["lycos@example.com", "aol@example.com", "excite@example.com"], + }, + { + name: "Fenix", + version: "20", + expected: ["aol@example.com", "altavista@example.com"], + }, + { + name: "Firefox", + version: "31", + expected: ["lycos@example.com", "aol@example.com"], + }, + { + name: "Firefox", + version: "30", + expected: ["lycos@example.com", "aol@example.com", "excite@example.com"], + }, + { + name: "Firefox", + version: "10", + expected: ["lycos@example.com", "aol@example.com", "excite@example.com"], + }, +]; + +add_setup(async function () { + Services.prefs.setBoolPref("browser.search.newSearchConfig.enabled", false); + await SearchTestUtils.useTestEngines("data", null, CONFIG); + await AddonTestUtils.promiseStartupManager(); + + let confUrl = `data:application/json,${JSON.stringify(CONFIG)}`; + Services.prefs.setStringPref("search.config.url", confUrl); +}); + +add_task(async function test_application_name() { + for (const { name, version, expected } of tests) { + let { engines } = await engineSelector.fetchEngineConfiguration({ + locale: "default", + region: "default", + name, + version, + }); + const engineIds = engines.map(obj => obj.webExtension.id); + Assert.deepEqual( + engineIds, + expected, + `Should have the expected engines for app: "${name}" + and version: "${version}"` + ); + } +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_old_selector_order.js b/toolkit/components/search/tests/xpcshell/test_engine_old_selector_order.js new file mode 100644 index 0000000000..6a7b63f9a2 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_old_selector_order.js @@ -0,0 +1,137 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SearchEngineSelectorOld: + "resource://gre/modules/SearchEngineSelectorOld.sys.mjs", +}); + +/** + * This constant defines the tests for the order. The input is an array of + * engines that will be constructed. The engine definitions are arrays with + * fields in order: + * name, orderHint, default, defaultPrivate + * + * The expected is an array of engine names. + */ +const TESTS = [ + { + // Basic tests to ensure correct order for default engine. + input: [ + ["A", 750, "no", "no"], + ["B", 3000, "no", "no"], + ["C", 2000, "yes", "no"], + ["D", 1000, "yes-if-no-other", "no"], + ["E", 500, "no", "no"], + ], + expected: ["C", "B", "D", "A", "E"], + expectedPrivate: undefined, + }, + { + input: [ + ["A", 750, "no", "no"], + ["B", 3000, "no", "no"], + ["C", 2000, "yes-if-no-other", "no"], + ["D", 1000, "yes", "no"], + ["E", 500, "no", "no"], + ], + expected: ["D", "B", "C", "A", "E"], + expectedPrivate: undefined, + }, + // Check that yes-if-no-other works correctly. + { + input: [ + ["A", 750, "no", "no"], + ["B", 3000, "no", "no"], + ["C", 2000, "no", "no"], + ["D", 1000, "yes-if-no-other", "no"], + ["E", 500, "no", "no"], + ], + expected: ["D", "B", "C", "A", "E"], + expectedPrivate: undefined, + }, + // Basic tests to ensure correct order with private engine. + { + input: [ + ["A", 750, "no", "no"], + ["B", 3000, "yes-if-no-other", "no"], + ["C", 2000, "no", "yes"], + ["D", 1000, "yes", "yes-if-no-other"], + ["E", 500, "no", "no"], + ], + expected: ["D", "C", "B", "A", "E"], + expectedPrivate: "C", + }, + { + input: [ + ["A", 750, "no", "yes-if-no-other"], + ["B", 3000, "yes-if-no-other", "no"], + ["C", 2000, "no", "yes"], + ["D", 1000, "yes", "no"], + ["E", 500, "no", "no"], + ], + expected: ["D", "C", "B", "A", "E"], + expectedPrivate: "C", + }, + // Private engine test for yes-if-no-other. + { + input: [ + ["A", 750, "no", "yes-if-no-other"], + ["B", 3000, "yes-if-no-other", "no"], + ["C", 2000, "no", "no"], + ["D", 1000, "yes", "no"], + ["E", 500, "no", "no"], + ], + expected: ["D", "A", "B", "C", "E"], + expectedPrivate: "A", + }, +]; + +function getConfigData(testInput) { + return testInput.map(info => ({ + engineName: info[0], + orderHint: info[1], + default: info[2], + defaultPrivate: info[3], + appliesTo: [ + { + included: { everywhere: true }, + }, + ], + })); +} + +const engineSelector = new SearchEngineSelectorOld(); + +add_task(async function () { + const settings = await RemoteSettings(SearchUtils.OLD_SETTINGS_KEY); + const getStub = sinon.stub(settings, "get"); + + let i = 0; + for (const test of TESTS) { + // Remove the existing configuration and update the stub to return the data + // for this test. + delete engineSelector._configuration; + getStub.returns(getConfigData(test.input)); + + const { engines, privateDefault } = + await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "us", + }); + + let names = engines.map(obj => obj.engineName); + Assert.deepEqual( + names, + test.expected, + `Should have the correct order for the engines: test ${i}` + ); + Assert.equal( + privateDefault && privateDefault.engineName, + test.expectedPrivate, + `Should have the correct selection for the private engine: test ${i++}` + ); + } +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_old_selector_override.js b/toolkit/components/search/tests/xpcshell/test_engine_old_selector_override.js new file mode 100644 index 0000000000..b4575be9ce --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_old_selector_override.js @@ -0,0 +1,196 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SearchEngineSelectorOld: + "resource://gre/modules/SearchEngineSelectorOld.sys.mjs", +}); + +const TEST_CONFIG = [ + { + webExtension: { + id: "aol@example.com", + }, + appliesTo: [ + { + included: { everywhere: true }, + }, + { + override: true, + application: { + distributions: ["distro1"], + }, + params: { + searchUrlGetParams: [ + { + name: "field-keywords", + value: "{searchTerms}", + }, + ], + }, + }, + ], + default: "yes-if-no-other", + }, + { + webExtension: { + id: "lycos@example.com", + }, + appliesTo: [ + { + included: { everywhere: true }, + }, + { + override: true, + experiment: "experiment1", + params: { + searchUrlGetParams: [ + { + name: "experiment-params", + value: "{searchTerms}", + }, + ], + }, + }, + ], + default: "yes", + }, + { + webExtension: { + id: "altavista@example.com", + }, + appliesTo: [ + { + included: { everywhere: true }, + }, + { + override: true, + application: { + distributions: ["distro2"], + }, + included: { regions: ["gb"] }, + params: { + searchUrlGetParams: [ + { + name: "field-keywords2", + value: "{searchTerms}", + }, + ], + }, + }, + ], + default: "yes", + }, +]; + +const engineSelector = new SearchEngineSelectorOld(); + +add_setup(async function () { + const settings = await RemoteSettings(SearchUtils.OLD_SETTINGS_KEY); + sinon.stub(settings, "get").returns(TEST_CONFIG); +}); + +add_task(async function test_engine_selector_defaults() { + // Check that with no override sections matching, we have no overrides active. + const { engines } = await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "us", + }); + + let engine = engines.find(e => e.webExtension.id == "aol@example.com"); + + Assert.ok( + !("params" in engine), + "Should not have overriden the parameters of the aol engine." + ); + + engine = engines.find(e => e.webExtension.id == "lycos@example.com"); + + Assert.ok( + !("params" in engine), + "Should not have overriden the parameters of the lycos engine." + ); +}); + +add_task(async function test_engine_selector_override_distributions() { + const { engines } = await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "us", + distroID: "distro1", + }); + + let engine = engines.find(e => e.webExtension.id == "aol@example.com"); + + Assert.deepEqual( + engine.params, + { + searchUrlGetParams: [ + { + name: "field-keywords", + value: "{searchTerms}", + }, + ], + }, + "Should have overriden the parameters of the engine." + ); +}); + +add_task(async function test_engine_selector_override_experiments() { + const { engines } = await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "us", + experiment: "experiment1", + }); + + let engine = engines.find(e => e.webExtension.id == "lycos@example.com"); + + Assert.deepEqual( + engine.params, + { + searchUrlGetParams: [ + { + name: "experiment-params", + value: "{searchTerms}", + }, + ], + }, + "Should have overriden the parameters of the engine." + ); +}); + +add_task(async function test_engine_selector_override_with_included() { + let { engines } = await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "us", + distroID: "distro2", + }); + + let engine = engines.find(e => e.webExtension.id == "altavista@example.com"); + Assert.ok( + !("params" in engine), + "Should not have overriden the parameters of the engine." + ); + + let result = await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "gb", + distroID: "distro2", + }); + engine = result.engines.find( + e => e.webExtension.id == "altavista@example.com" + ); + Assert.deepEqual( + engine.params, + { + searchUrlGetParams: [ + { + name: "field-keywords2", + value: "{searchTerms}", + }, + ], + }, + "Should have overriden the parameters of the engine." + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_old_selector_remote_override.js b/toolkit/components/search/tests/xpcshell/test_engine_old_selector_remote_override.js new file mode 100644 index 0000000000..9535adfb28 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_old_selector_remote_override.js @@ -0,0 +1,135 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SearchEngineSelectorOld: + "resource://gre/modules/SearchEngineSelectorOld.sys.mjs", + SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs", +}); + +const TEST_CONFIG_OLD = [ + { + engineName: "aol", + telemetryId: "aol", + appliesTo: [ + { + included: { everywhere: true }, + }, + ], + params: { + searchUrlGetParams: [ + { + name: "original_param", + value: "original_value", + }, + ], + }, + }, +]; + +const TEST_CONFIG_OVERRIDE_OLD = [ + { + telemetryId: "aol", + telemetrySuffix: "tsfx", + params: { + searchUrlGetParams: [ + { + name: "new_param", + value: "new_value", + }, + ], + }, + }, +]; + +const TEST_CONFIG = [ + { + base: { + urls: { + search: { + base: "https://www.bing.com/search", + params: [ + { + name: "old_param", + value: "old_value", + }, + ], + searchTermParamName: "q", + }, + }, + }, + variants: [ + { + environment: { + locales: ["en-US"], + }, + }, + ], + identifier: "aol", + recordType: "engine", + }, + { + recordType: "defaultEngines", + globalDefault: "aol", + specificDefaults: [], + }, + { + orders: [], + recordType: "engineOrders", + }, +]; + +const TEST_CONFIG_OVERRIDE = [ + { + identifier: "aol", + urls: { + search: { + params: [{ name: "new_param", value: "new_value" }], + }, + }, + telemetrySuffix: "tsfx", + clickUrl: "https://aol.url", + }, +]; + +const engineSelectorOld = new SearchEngineSelectorOld(); +const engineSelector = new SearchEngineSelector(); + +add_setup(async function () { + const settingsOld = await RemoteSettings(SearchUtils.OLD_SETTINGS_KEY); + sinon.stub(settingsOld, "get").returns(TEST_CONFIG_OLD); + const overridesOld = await RemoteSettings( + SearchUtils.OLD_SETTINGS_OVERRIDES_KEY + ); + sinon.stub(overridesOld, "get").returns(TEST_CONFIG_OVERRIDE_OLD); + + const settings = await RemoteSettings(SearchUtils.NEW_SETTINGS_KEY); + sinon.stub(settings, "get").returns(TEST_CONFIG); + const overrides = await RemoteSettings( + SearchUtils.NEW_SETTINGS_OVERRIDES_KEY + ); + sinon.stub(overrides, "get").returns(TEST_CONFIG_OVERRIDE); +}); + +add_task(async function test_engine_selector_old() { + let { engines } = await engineSelectorOld.fetchEngineConfiguration({ + locale: "en-US", + region: "us", + }); + Assert.equal(engines[0].telemetryId, "aol-tsfx"); + Assert.equal(engines[0].params.searchUrlGetParams[0].name, "new_param"); + Assert.equal(engines[0].params.searchUrlGetParams[0].value, "new_value"); +}); + +add_task(async function test_engine_selector() { + let { engines } = await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "us", + }); + Assert.equal(engines[0].telemetrySuffix, "tsfx"); + Assert.equal(engines[0].clickUrl, "https://aol.url"); + Assert.equal(engines[0].urls.search.params[0].name, "new_param"); + Assert.equal(engines[0].urls.search.params[0].value, "new_value"); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_old_selector_remote_settings.js b/toolkit/components/search/tests/xpcshell/test_engine_old_selector_remote_settings.js new file mode 100644 index 0000000000..622db4a291 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_old_selector_remote_settings.js @@ -0,0 +1,345 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SearchEngineSelectorOld: + "resource://gre/modules/SearchEngineSelectorOld.sys.mjs", +}); + +const TEST_CONFIG = [ + { + engineName: "aol", + orderHint: 500, + webExtension: { + locales: ["default"], + }, + appliesTo: [ + { + included: { everywhere: true }, + }, + { + included: { regions: ["us"] }, + webExtension: { + locales: ["$USER_LOCALE"], + }, + }, + ], + }, + { + engineName: "lycos", + orderHint: 1000, + default: "yes", + appliesTo: [ + { + included: { everywhere: true }, + excluded: { locales: { matches: ["zh-CN"] } }, + }, + ], + }, + { + engineName: "altavista", + orderHint: 2000, + defaultPrivate: "yes", + appliesTo: [ + { + included: { locales: { matches: ["en-US"] } }, + }, + { + included: { regions: ["default"] }, + }, + ], + }, + { + engineName: "excite", + default: "yes-if-no-other", + appliesTo: [ + { + included: { everywhere: true }, + excluded: { regions: ["us"] }, + }, + { + included: { everywhere: true }, + cohort: "acohortid", + }, + ], + }, + { + engineName: "askjeeves", + }, +]; + +let getStub; + +add_setup(async function () { + const searchConfigSettings = await RemoteSettings( + SearchUtils.OLD_SETTINGS_KEY + ); + getStub = sinon.stub(searchConfigSettings, "get"); + + // We expect this error from remove settings as we're invalidating the + // signature. + consoleAllowList.push("Invalid content signature (abc)"); + // We also test returning an empty configuration. + consoleAllowList.push("Received empty search configuration"); +}); + +add_task(async function test_selector_basic_get() { + const listenerSpy = sinon.spy(); + const engineSelector = new SearchEngineSelectorOld(listenerSpy); + getStub.onFirstCall().returns(TEST_CONFIG); + + const { engines } = await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "default", + }); + + Assert.deepEqual( + engines.map(e => e.engineName), + ["lycos", "altavista", "aol", "excite"], + "Should have obtained the correct data from the database." + ); + Assert.ok(listenerSpy.notCalled, "Should not have called the listener"); +}); + +add_task(async function test_selector_get_reentry() { + const listenerSpy = sinon.spy(); + const engineSelector = new SearchEngineSelectorOld(listenerSpy); + let promise = Promise.withResolvers(); + getStub.resetHistory(); + getStub.onFirstCall().returns(promise.promise); + delete engineSelector._configuration; + + let firstResult; + let secondResult; + + const firstCallPromise = engineSelector + .fetchEngineConfiguration({ + locale: "en-US", + region: "default", + }) + .then(result => (firstResult = result.engines)); + + const secondCallPromise = engineSelector + .fetchEngineConfiguration({ + locale: "en-US", + region: "default", + }) + .then(result => (secondResult = result.engines)); + + Assert.strictEqual( + firstResult, + undefined, + "Should not have returned the first result yet." + ); + + Assert.strictEqual( + secondResult, + undefined, + "Should not have returned the second result yet." + ); + + promise.resolve(TEST_CONFIG); + + await Promise.all([firstCallPromise, secondCallPromise]); + Assert.deepEqual( + firstResult.map(e => e.engineName), + ["lycos", "altavista", "aol", "excite"], + "Should have returned the correct data to the first call" + ); + + Assert.deepEqual( + secondResult.map(e => e.engineName), + ["lycos", "altavista", "aol", "excite"], + "Should have returned the correct data to the second call" + ); + Assert.ok(listenerSpy.notCalled, "Should not have called the listener"); +}); + +add_task(async function test_selector_config_update() { + const listenerSpy = sinon.spy(); + const engineSelector = new SearchEngineSelectorOld(listenerSpy); + getStub.resetHistory(); + getStub.onFirstCall().returns(TEST_CONFIG); + + const { engines } = await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "default", + }); + + Assert.deepEqual( + engines.map(e => e.engineName), + ["lycos", "altavista", "aol", "excite"], + "Should have got the correct configuration" + ); + + Assert.ok(listenerSpy.notCalled, "Should not have called the listener yet"); + + const NEW_DATA = [ + { + default: "yes", + engineName: "askjeeves", + appliesTo: [{ included: { everywhere: true } }], + schema: 1553857697843, + last_modified: 1553859483588, + }, + ]; + + getStub.resetHistory(); + getStub.onFirstCall().returns(NEW_DATA); + await RemoteSettings(SearchUtils.OLD_SETTINGS_KEY).emit("sync", { + data: { + current: NEW_DATA, + }, + }); + + Assert.ok(listenerSpy.called, "Should have called the listener"); + + const result = await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "default", + }); + + Assert.deepEqual( + result.engines.map(e => e.engineName), + ["askjeeves"], + "Should have updated the configuration with the new data" + ); +}); + +add_task(async function test_selector_db_modification() { + const engineSelector = new SearchEngineSelectorOld(); + // Fill the database with some values that we can use to test that it is cleared. + const db = RemoteSettings(SearchUtils.OLD_SETTINGS_KEY).db; + await db.importChanges( + {}, + Date.now(), + [ + { + id: "85e1f268-9ca5-4b52-a4ac-922df5c07264", + default: "yes", + engineName: "askjeeves", + appliesTo: [{ included: { everywhere: true } }], + }, + ], + { clear: true } + ); + + // Stub the get() so that the first call simulates a signature error, and + // the second simulates success reading from the dump. + getStub.resetHistory(); + getStub + .onFirstCall() + .rejects(new RemoteSettingsClient.InvalidSignatureError("abc")); + getStub.onSecondCall().returns(TEST_CONFIG); + + let result = await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "default", + }); + + Assert.ok( + getStub.calledTwice, + "Should have called the get() function twice." + ); + + const databaseEntries = await db.list(); + Assert.equal(databaseEntries.length, 0, "Should have cleared the database."); + + Assert.deepEqual( + result.engines.map(e => e.engineName), + ["lycos", "altavista", "aol", "excite"], + "Should have returned the correct data." + ); +}); + +add_task(async function test_selector_db_modification_never_succeeds() { + const engineSelector = new SearchEngineSelectorOld(); + // Fill the database with some values that we can use to test that it is cleared. + const db = RemoteSettings(SearchUtils.OLD_SETTINGS_KEY).db; + await db.importChanges( + {}, + Date.now(), + [ + { + id: "b70edfdd-1c3f-4b7b-ab55-38cb048636c0", + default: "yes", + engineName: "askjeeves", + appliesTo: [{ included: { everywhere: true } }], + }, + ], + { + clear: true, + } + ); + + // Now simulate the condition where for some reason we never get a + // valid result. + getStub.reset(); + getStub.rejects(new RemoteSettingsClient.InvalidSignatureError("abc")); + + await Assert.rejects( + engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "default", + }), + ex => ex.result == Cr.NS_ERROR_UNEXPECTED, + "Should have rejected loading the engine configuration" + ); + + Assert.ok( + getStub.calledTwice, + "Should have called the get() function twice." + ); + + const databaseEntries = await db.list(); + Assert.equal(databaseEntries.length, 0, "Should have cleared the database."); +}); + +add_task(async function test_empty_results() { + // Check that returning an empty result re-tries. + const engineSelector = new SearchEngineSelectorOld(); + // Fill the database with some values that we can use to test that it is cleared. + const db = RemoteSettings(SearchUtils.OLD_SETTINGS_KEY).db; + await db.importChanges( + {}, + Date.now(), + [ + { + id: "df5655ca-e045-4f8c-a7ee-047eeb654722", + default: "yes", + engineName: "askjeeves", + appliesTo: [{ included: { everywhere: true } }], + }, + ], + { + clear: true, + } + ); + + // Stub the get() so that the first call simulates an empty database, and + // the second simulates success reading from the dump. + getStub.resetHistory(); + getStub.onFirstCall().returns([]); + getStub.onSecondCall().returns(TEST_CONFIG); + + let result = await engineSelector.fetchEngineConfiguration({ + locale: "en-US", + region: "default", + }); + + Assert.ok( + getStub.calledTwice, + "Should have called the get() function twice." + ); + + const databaseEntries = await db.list(); + Assert.equal(databaseEntries.length, 0, "Should have cleared the database."); + + Assert.deepEqual( + result.engines.map(e => e.engineName), + ["lycos", "altavista", "aol", "excite"], + "Should have returned the correct data." + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_selector_defaults.js b/toolkit/components/search/tests/xpcshell/test_engine_selector_defaults.js new file mode 100644 index 0000000000..bb904fcc86 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_selector_defaults.js @@ -0,0 +1,349 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests the SearchEngineSelector in finding the correct default engine + * and private default engine based on the user's environment. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs", +}); + +const CONFIG = [ + { + recordType: "engine", + identifier: "global-default", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + }, + { + recordType: "engine", + identifier: "global-default-private", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + }, + { + recordType: "engine", + identifier: "default-specific-location", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + }, + { + recordType: "engine", + identifier: "default-specific-distro", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + }, + { + recordType: "engine", + identifier: "default-starts-with-ca", + base: {}, + variants: [ + { + environment: { locales: ["en-US"], regions: ["CA"] }, + }, + ], + }, + { + recordType: "engine", + identifier: "default-starts-with-us", + base: {}, + variants: [ + { + environment: { locales: ["en-US"], regions: ["US"] }, + }, + ], + }, + { + recordType: "defaultEngines", + globalDefault: "global-default", + globalDefaultPrivate: "global-default-private", + specificDefaults: [ + { + environment: { locales: ["zh-CN"], regions: ["cn"] }, + default: "default-specific-location", + defaultPrivate: "default-specific-location", + }, + { + environment: { distributions: ["specific-distro"] }, + default: "default-specific-distro", + defaultPrivate: "default-specific-distro", + }, + { + environment: { locales: ["en-US"], regions: ["CA"] }, + default: "default-starts-with*", + defaultPrivate: "default-starts-with*", + }, + { + environment: { locales: ["en-US"], regions: ["US"] }, + default: "default-starts-with*", + defaultPrivate: "default-starts-with*", + }, + ], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + +const CONFIG_DEFAULTS_OVERRIDE = [ + { + recordType: "engine", + identifier: "engine-global", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-locale-de", + base: {}, + variants: [ + { + environment: { + locales: ["de"], + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-distro", + base: {}, + variants: [ + { + environment: { + distributions: ["distro"], + regions: ["FR"], + }, + }, + ], + }, + { + recordType: "defaultEngines", + globalDefault: "engine-global", + specificDefaults: [ + { + environment: { locales: ["de"] }, + default: "engine-locale-de", + }, + { + environment: { distributions: ["distro"] }, + default: "engine-distro", + }, + ], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + +const engineSelector = new SearchEngineSelector(); +let settings; +let settingOverrides; +let configStub; +let overrideStub; + +/** + * This function asserts if the actual engine identifiers returned equals + * the expected engines. + * + * @param {object} config + * A mock search config contain engines. + * @param {object} userEnv + * A fake user's environment including locale and region, experiment, etc. + * @param {string} expectedDefault + * The identifer of the expected default engine. + * @param {string} expectedDefaultPrivate + * The identifer of the expected default private engine. + * @param {string} message + * The description of the test. + */ +async function assertActualEnginesEqualsExpected( + config, + userEnv, + expectedDefault, + expectedDefaultPrivate, + message +) { + engineSelector._configuration = null; + configStub.returns(config); + + let { engines, privateDefault } = + await engineSelector.fetchEngineConfiguration(userEnv); + let actualEngines = engines.map(engine => engine.identifier); + + info(`${message}`); + Assert.equal( + actualEngines[0], + expectedDefault, + `Should match the default engine ${expectedDefault}.` + ); + + Assert.equal( + privateDefault ? privateDefault.identifier : undefined, + expectedDefaultPrivate, + `Should match default private engine ${expectedDefaultPrivate}.` + ); +} + +add_setup(async function () { + settings = await RemoteSettings(SearchUtils.NEW_SETTINGS_KEY); + configStub = sinon.stub(settings, "get"); + settingOverrides = await RemoteSettings( + SearchUtils.NEW_SETTINGS_OVERRIDES_KEY + ); + overrideStub = sinon.stub(settingOverrides, "get"); + overrideStub.returns([]); +}); + +add_task(async function test_default_engines() { + await assertActualEnginesEqualsExpected( + CONFIG, + { + locale: "en-CA", + region: "ca", + }, + "global-default", + "global-default-private", + "Should use the global default engine and global default private when no specific defaults are matched." + ); + + await assertActualEnginesEqualsExpected( + CONFIG, + { + locale: "zh-CN", + region: "cn", + }, + "default-specific-location", + "default-specific-location", + "Should use the matched locale and region default engine." + ); + + await assertActualEnginesEqualsExpected( + CONFIG, + { + locale: "fi", + region: "FI", + distroID: "specific-distro", + }, + "default-specific-distro", + "default-specific-distro", + "Should use the matched distribution default engine." + ); + + await assertActualEnginesEqualsExpected( + CONFIG, + { + locale: "en-US", + region: "CA", + }, + "default-starts-with-ca", + "default-starts-with-ca", + "Should use the matched default engine with specific suffix." + ); + + await assertActualEnginesEqualsExpected( + CONFIG, + { + locale: "en-US", + region: "US", + }, + "default-starts-with-us", + "default-starts-with-us", + "Should use the matched default engine with specific suffix." + ); +}); + +add_task(async function test_default_engines_override() { + await assertActualEnginesEqualsExpected( + CONFIG_DEFAULTS_OVERRIDE, + { + locale: "en-US", + region: "US", + }, + "engine-global", + undefined, + "Should use the globalDefault for default when no specific defaults are matched. Private default should be undefined when no default private." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_DEFAULTS_OVERRIDE, + { + locale: "de", + region: "US", + }, + "engine-locale-de", + undefined, + "Should use the matched locale default engine." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_DEFAULTS_OVERRIDE, + { + locale: "de", + region: "FR", + }, + "engine-locale-de", + undefined, + "Should use the matched locale default engine." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_DEFAULTS_OVERRIDE, + { + locale: "en-US", + region: "FR", + distroID: "distro", + }, + "engine-distro", + undefined, + "Should use the matched distro default engine." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_DEFAULTS_OVERRIDE, + { + locale: "de", + region: "FR", + distroID: "distro", + }, + "engine-distro", + undefined, + "Should use the last matched default engine when multiple default engines are matched." + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_selector_engine_orders.js b/toolkit/components/search/tests/xpcshell/test_engine_selector_engine_orders.js new file mode 100644 index 0000000000..e7725ceb21 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_selector_engine_orders.js @@ -0,0 +1,348 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests the SearchEngineSelector in ordering the engines correctly based + * on the user's environment. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs", +}); + +const ENGINE_ORDERS_CONFIG = [ + { + recordType: "engine", + identifier: "b-engine", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + }, + { + recordType: "engine", + identifier: "a-engine", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + }, + { + recordType: "engine", + identifier: "c-engine", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + }, + { + recordType: "defaultEngines", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [ + { + environment: { distributions: ["distro"] }, + order: ["a-engine", "b-engine", "c-engine"], + }, + { + environment: { + distributions: ["distro"], + locales: ["en-CA"], + regions: ["CA"], + }, + order: ["c-engine", "b-engine", "a-engine"], + }, + { + environment: { + distributions: ["distro-2"], + }, + order: ["a-engine", "b-engine"], + }, + ], + }, +]; + +const STARTS_WITH_WIKI_CONFIG = [ + { + recordType: "engine", + identifier: "wiki-ca", + base: {}, + variants: [ + { + environment: { + locales: ["en-CA"], + regions: ["CA"], + }, + }, + ], + }, + { + recordType: "engine", + identifier: "wiki-uk", + base: {}, + variants: [ + { + environment: { + locales: ["en-GB"], + regions: ["GB"], + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-1", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-2", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + }, + { + recordType: "defaultEngines", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [ + { + environment: { + locales: ["en-CA"], + regions: ["CA"], + }, + order: ["wiki*", "engine-1", "engine-2"], + }, + { + environment: { + locales: ["en-GB"], + regions: ["GB"], + }, + order: ["wiki*", "engine-1", "engine-2"], + }, + ], + }, +]; + +const DEFAULTS_CONFIG = [ + { + recordType: "engine", + identifier: "b-engine", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + }, + { + recordType: "engine", + identifier: "a-engine", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + }, + { + recordType: "engine", + identifier: "default-engine", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + }, + { + recordType: "engine", + identifier: "default-private-engine", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + }, + { + recordType: "defaultEngines", + globalDefault: "default-engine", + globalDefaultPrivate: "default-private-engine", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [ + { + environment: { + locales: ["en-CA"], + regions: ["CA"], + }, + order: ["a-engine", "b-engine"], + }, + ], + }, +]; + +const engineSelector = new SearchEngineSelector(); +let settings; +let settingOverrides; +let configStub; +let overrideStub; + +/** + * This function asserts if the actual engine identifiers returned equals + * the expected engines. + * + * @param {object} config + * A mock search config contain engines. + * @param {object} userEnv + * A fake user's environment including locale and region, experiment, etc. + * @param {Array} expectedEngineOrders + * The array of engine identifers in the expected order. + * @param {string} message + * The description of the test. + */ +async function assertActualEnginesEqualsExpected( + config, + userEnv, + expectedEngineOrders, + message +) { + engineSelector._configuration = null; + configStub.returns(config); + + let { engines } = await engineSelector.fetchEngineConfiguration(userEnv); + let actualEngineOrders = engines.map(engine => engine.identifier); + + info(`${message}`); + Assert.deepEqual(actualEngineOrders, expectedEngineOrders, message); +} + +add_setup(async function () { + settings = await RemoteSettings(SearchUtils.NEW_SETTINGS_KEY); + configStub = sinon.stub(settings, "get"); + settingOverrides = await RemoteSettings( + SearchUtils.NEW_SETTINGS_OVERRIDES_KEY + ); + overrideStub = sinon.stub(settingOverrides, "get"); + overrideStub.returns([]); +}); + +add_task(async function test_selector_match_engine_orders() { + await assertActualEnginesEqualsExpected( + ENGINE_ORDERS_CONFIG, + { + locale: "fr", + region: "FR", + distroID: "distro", + }, + ["a-engine", "b-engine", "c-engine"], + "Should match engine orders with the distro distribution." + ); + + await assertActualEnginesEqualsExpected( + ENGINE_ORDERS_CONFIG, + { + locale: "en-CA", + region: "CA", + distroID: "distro", + }, + ["c-engine", "b-engine", "a-engine"], + "Should match engine orders with the distro distribution, en-CA locale and CA region." + ); + + await assertActualEnginesEqualsExpected( + ENGINE_ORDERS_CONFIG, + { + locale: "en-CA", + region: "CA", + distroID: "distro-2", + }, + ["a-engine", "b-engine", "c-engine"], + "Should order the first two engines correctly for distro-2 distribution" + ); + + await assertActualEnginesEqualsExpected( + ENGINE_ORDERS_CONFIG, + { + locale: "en-CA", + region: "CA", + }, + ["b-engine", "a-engine", "c-engine"], + "Should be in the same engine order as the config when there's no engine order environments matched." + ); +}); + +add_task(async function test_selector_match_engine_orders_starts_with() { + await assertActualEnginesEqualsExpected( + STARTS_WITH_WIKI_CONFIG, + { + locale: "en-CA", + region: "CA", + }, + ["wiki-ca", "engine-1", "engine-2"], + "Should list the wiki-ca engine and other engines in correct orders with the en-CA and CA locale region environment." + ); + + await assertActualEnginesEqualsExpected( + STARTS_WITH_WIKI_CONFIG, + { + locale: "en-GB", + region: "GB", + }, + ["wiki-uk", "engine-1", "engine-2"], + "Should list the wiki-ca engine and other engines in correct orders with the en-CA and CA locale region environment." + ); +}); + +add_task(async function test_selector_match_engine_orders_with_defaults() { + await assertActualEnginesEqualsExpected( + DEFAULTS_CONFIG, + { + locale: "en-CA", + region: "CA", + }, + ["default-engine", "default-private-engine", "a-engine", "b-engine"], + "Should order the default engine first, default private engine second, and the rest of the engines in the correct order." + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_selector_environment.js b/toolkit/components/search/tests/xpcshell/test_engine_selector_environment.js new file mode 100644 index 0000000000..bf56984cde --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_selector_environment.js @@ -0,0 +1,795 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests the SearchEngineSelector's functionality in correctly filtering the + * engines from the config based on the user's environment. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SearchEngineSelector: "resource://gre/modules/SearchEngineSelector.sys.mjs", +}); + +const CONFIG_EVERYWHERE = [ + { + recordType: "engine", + identifier: "engine-everywhere", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-everywhere-except-en-US", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + excludedLocales: ["en-US"], + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-everywhere-except-FI", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + excludedRegions: ["FI"], + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-everywhere-except-en-CA-and-CA", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + excludedRegions: ["CA"], + excludedLocales: ["en-CA"], + }, + }, + ], + }, + { + recordType: "defaultEngines", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + +const CONFIG_EXPERIMENT = [ + { + recordType: "engine", + identifier: "engine-experiment", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + experiment: "experiment", + }, + }, + ], + }, + { + recordType: "defaultEngines", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + +const CONFIG_LOCALES_AND_REGIONS = [ + { + recordType: "engine", + identifier: "engine-canada", + base: {}, + variants: [ + { + environment: { + locales: ["en-CA"], + regions: ["CA"], + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-exclude-regions", + base: {}, + variants: [ + { + environment: { + locales: ["en-GB"], + excludedRegions: ["US"], + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-specific-locale-in-all-regions", + base: {}, + variants: [ + { + environment: { + locales: ["en-US"], + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-exclude-locale", + base: {}, + variants: [ + { + environment: { + excludedLocales: ["fr"], + regions: ["BE"], + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-specific-region-with-any-locales", + base: {}, + variants: [ + { + environment: { + regions: ["FI"], + }, + }, + ], + }, + { + recordType: "defaultEngines", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + +const CONFIG_DISTRIBUTION = [ + { + recordType: "engine", + identifier: "engine-distribution-1", + base: {}, + variants: [ + { + environment: { + distributions: ["distribution-1"], + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-multiple-distributions", + base: {}, + variants: [ + { + environment: { + distributions: ["distribution-2", "distribution-3"], + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-distribution-region-locales", + base: {}, + variants: [ + { + environment: { + distributions: ["distribution-4"], + locales: ["fi"], + regions: ["FI"], + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-distribution-experiment", + base: {}, + variants: [ + { + environment: { + distributions: ["distribution-5"], + experiment: "experiment", + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-distribution-excluded", + base: {}, + variants: [ + { + environment: { + distributions: ["distribution-include"], + excludedDistributions: ["distribution-exclude"], + }, + }, + ], + }, + { + recordType: "defaultEngines", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + +const CONFIG_CHANNEL_APPLICATION = [ + { + recordType: "engine", + identifier: "engine-channel", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + channels: ["esr"], + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-application", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + applications: ["firefox"], + }, + }, + ], + }, + { + recordType: "defaultEngines", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + +const CONFIG_OPTIONAL = [ + { + recordType: "engine", + identifier: "engine-optional-true", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + applications: ["firefox"], + }, + optional: true, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-optional-false", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + applications: ["firefox"], + }, + optional: false, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-optional-undefined", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + applications: ["firefox"], + }, + }, + ], + }, + { + recordType: "defaultEngines", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + +const CONFIG_VERSIONS = [ + { + recordType: "engine", + identifier: "engine-min-1", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + minVersion: "1", + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-max-20", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + maxVersion: "20", + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-min-max-1-10", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + minVersion: "1", + maxVersion: "10", + }, + }, + ], + }, + { + recordType: "defaultEngines", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + +const engineSelector = new SearchEngineSelector(); +let settings; +let settingOverrides; +let configStub; +let overrideStub; + +/** + * This function asserts if the actual engine identifiers returned equals + * the expected engines. + * + * @param {object} config + * A mock search config contain engines. + * @param {object} userEnv + * A fake user's environment including locale and region, experiment, etc. + * @param {Array} expectedEngines + * The array of expected engine identifiers to be returned from the config. + * @param {string} message + * The assertion message. + */ +async function assertActualEnginesEqualsExpected( + config, + userEnv, + expectedEngines, + message +) { + engineSelector._configuration = null; + configStub.returns(config); + + let { engines } = await engineSelector.fetchEngineConfiguration(userEnv); + let actualEngines = engines.map(engine => engine.identifier); + Assert.deepEqual(actualEngines, expectedEngines, message); +} + +add_setup(async function () { + settings = await RemoteSettings(SearchUtils.NEW_SETTINGS_KEY); + configStub = sinon.stub(settings, "get"); + settingOverrides = await RemoteSettings( + SearchUtils.NEW_SETTINGS_OVERRIDES_KEY + ); + overrideStub = sinon.stub(settingOverrides, "get"); + overrideStub.returns([]); +}); + +add_task(async function test_selector_match_experiment() { + await assertActualEnginesEqualsExpected( + CONFIG_EXPERIMENT, + { + locale: "en-CA", + region: "ca", + experiment: "experiment", + }, + ["engine-experiment"], + "Should match engine with the same experiment." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_EXPERIMENT, + { + locale: "en-CA", + region: "ca", + experiment: "no-match-experiment", + }, + [], + "Should not match any engines without experiment." + ); +}); + +add_task(async function test_everywhere_and_excluded_locale() { + await assertActualEnginesEqualsExpected( + CONFIG_EVERYWHERE, + { + locale: "en-GB", + region: "GB", + }, + [ + "engine-everywhere", + "engine-everywhere-except-en-US", + "engine-everywhere-except-FI", + "engine-everywhere-except-en-CA-and-CA", + ], + "Should match the engines for all locales and regions." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_EVERYWHERE, + { + locale: "en-US", + region: "US", + }, + [ + "engine-everywhere", + "engine-everywhere-except-FI", + "engine-everywhere-except-en-CA-and-CA", + ], + "Should match engines that do not exclude user's locale." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_EVERYWHERE, + { + locale: "fi", + region: "FI", + }, + [ + "engine-everywhere", + "engine-everywhere-except-en-US", + "engine-everywhere-except-en-CA-and-CA", + ], + "Should match engines that do not exclude user's region." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_EVERYWHERE, + { + locale: "en-CA", + region: "CA", + }, + [ + "engine-everywhere", + "engine-everywhere-except-en-US", + "engine-everywhere-except-FI", + ], + "Should match engine that do not exclude user's region and locale." + ); +}); + +add_task(async function test_selector_locales_and_regions() { + await assertActualEnginesEqualsExpected( + CONFIG_LOCALES_AND_REGIONS, + { + locale: "en-CA", + region: "CA", + }, + ["engine-canada"], + "Should match engine with same locale and region." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_LOCALES_AND_REGIONS, + { + locale: "en-GB", + region: "US", + }, + [], + "Should not match any engines when region is excluded." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_LOCALES_AND_REGIONS, + { + locale: "en-US", + region: "AU", + }, + ["engine-specific-locale-in-all-regions"], + "Should match engine with specified locale in any region." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_LOCALES_AND_REGIONS, + { + locale: "en-US", + region: "NL", + }, + ["engine-specific-locale-in-all-regions"], + "Should match engine with specified locale in any region." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_LOCALES_AND_REGIONS, + { + locale: "fr", + region: "BE", + }, + [], + "Should not match any engines when locale is excluded." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_LOCALES_AND_REGIONS, + { + locale: "fi", + region: "FI", + }, + ["engine-specific-region-with-any-locales"], + "Should match engine with specified region with any locale." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_LOCALES_AND_REGIONS, + { + locale: "tlh", + region: "FI", + }, + ["engine-specific-region-with-any-locales"], + "Should match engine with specified region with any locale." + ); +}); + +add_task(async function test_selector_match_distribution() { + await assertActualEnginesEqualsExpected( + CONFIG_DISTRIBUTION, + { + locale: "en-CA", + region: "CA", + distroID: "distribution-1", + }, + ["engine-distribution-1"], + "Should match engine with the same distribution." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_DISTRIBUTION, + { + locale: "en-CA", + region: "CA", + distroID: "distribution-2", + }, + ["engine-multiple-distributions"], + "Should match engine with multiple distributions." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_DISTRIBUTION, + { + locale: "en-CA", + region: "CA", + distroID: "distribution-3", + }, + ["engine-multiple-distributions"], + "Should match engine with multiple distributions." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_DISTRIBUTION, + { + locale: "fi", + region: "FI", + distroID: "distribution-4", + }, + ["engine-distribution-region-locales"], + "Should match engine with distribution, specific region and locale." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_DISTRIBUTION, + { + locale: "en-CA", + region: "CA", + distroID: "distribution-4", + }, + [], + "Should not match any engines with no matching distribution, region and locale." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_DISTRIBUTION, + { + locale: "en-CA", + region: "CA", + distroID: "distribution-5", + experiment: "experiment", + }, + ["engine-distribution-experiment"], + "Should match engine with distribution and experiment." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_DISTRIBUTION, + { + locale: "en-CA", + region: "CA", + distroID: "distribution-5", + experiment: "no-match-experiment", + }, + [], + "Should not match any engines with no matching distribution and experiment." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_DISTRIBUTION, + { + locale: "en-CA", + region: "CA", + distroID: "distribution-include", + }, + ["engine-distribution-excluded"], + "Should match engines with included distributions." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_DISTRIBUTION, + { + locale: "en-CA", + region: "CA", + distroID: "distribution-exclude", + }, + [], + "Should not match any engines with excluded distribution." + ); +}); + +add_task(async function test_engine_selector_match_applications() { + await assertActualEnginesEqualsExpected( + CONFIG_CHANNEL_APPLICATION, + { + locale: "en-CA", + region: "CA", + channel: "esr", + }, + ["engine-channel"], + "Should match engine for esr channel." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_CHANNEL_APPLICATION, + { + locale: "en-CA", + region: "CA", + appName: "firefox", + }, + ["engine-application"], + "Should match engine for application." + ); +}); + +add_task(async function test_engine_selector_match_version() { + await assertActualEnginesEqualsExpected( + CONFIG_VERSIONS, + { + locale: "en-CA", + region: "CA", + version: "1", + }, + ["engine-min-1", "engine-max-20", "engine-min-max-1-10"], + "Should match engines with that support versions equal or above the minimum." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_VERSIONS, + { + locale: "en-CA", + region: "CA", + version: "30", + }, + ["engine-min-1"], + "Should match engines with that support versions above the minimum." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_VERSIONS, + { + locale: "en-CA", + region: "CA", + version: "20", + }, + ["engine-min-1", "engine-max-20"], + "Should match engines with that support versions equal or below the maximum." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_VERSIONS, + { + locale: "en-CA", + region: "CA", + version: "5", + }, + ["engine-min-1", "engine-max-20", "engine-min-max-1-10"], + "Should match engines with that support the versions above the minimum and below the maximum." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_VERSIONS, + { + locale: "en-CA", + region: "CA", + version: "15", + }, + ["engine-min-1", "engine-max-20"], + "Should match engines with that support the versions above the minimum and below the maximum." + ); + + await assertActualEnginesEqualsExpected( + CONFIG_VERSIONS, + { + locale: "en-CA", + region: "CA", + version: "", + }, + [], + "Should match no engines with no matching versions." + ); +}); + +add_task(async function test_engine_selector_does_not_match_optional_engines() { + await assertActualEnginesEqualsExpected( + CONFIG_OPTIONAL, + { + locale: "en-CA", + region: "CA", + appName: "firefox", + }, + ["engine-optional-false", "engine-optional-undefined"], + "Should match engines where optional flag is false or undefined" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_selector_variants.js b/toolkit/components/search/tests/xpcshell/test_engine_selector_variants.js new file mode 100644 index 0000000000..0fd57f2094 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_selector_variants.js @@ -0,0 +1,203 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests the SearchEngineSelector matching the correct variant engine. + * If multiple variants match, it should add the matching variants to the base + * cumulatively. + */ + +"use strict"; + +const CONFIG = [ + { + recordType: "engine", + identifier: "engine-1", + base: {}, + urls: { + search: { + params: [ + { + name: "partner-code", + value: "code", + }, + ], + }, + }, + variants: [ + { + environment: { regions: ["CA", "GB", "IT"] }, + urls: { + search: { + params: [], + }, + }, + }, + { + environment: { regions: ["CA", "US"] }, + urls: { + search: { + params: [ + { + name: "partner-code", + value: "bar", + }, + ], + }, + }, + telemetrySuffix: "telemetry", + }, + { + environment: { regions: ["CA", "GB"] }, + urls: { + search: { + params: [ + { + name: "partner-code", + value: "foo", + }, + ], + }, + }, + searchTermParamName: "search-param", + }, + ], + }, + { + recordType: "defaultEngines", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + +const CONFIG_CLONE = structuredClone(CONFIG); + +const engineSelector = new SearchEngineSelector(); +let settings; +let configStub; + +/** + * This function asserts if the actual engines returned equals the expected + * engines. + * + * @param {object} config + * A fake search config containing engines. + * @param {object} userEnv + * A fake user's environment including locale and region, experiment, etc. + * @param {Array} expectedEngines + * The array of expected engines to be returned from the fake config. + * @param {string} message + * The assertion message. + */ +async function assertActualEnginesEqualsExpected( + config, + userEnv, + expectedEngines, + message +) { + engineSelector._configuration = null; + configStub.returns(config); + let { engines } = await engineSelector.fetchEngineConfiguration(userEnv); + + Assert.deepEqual(engines, expectedEngines, message); +} + +add_setup(async function () { + settings = await RemoteSettings(SearchUtils.NEW_SETTINGS_KEY); + configStub = sinon.stub(settings, "get"); +}); + +add_task(async function test_no_variants_match() { + await assertActualEnginesEqualsExpected( + CONFIG, + { + locale: "fi", + region: "FI", + }, + [], + "Should match no variants." + ); +}); + +add_task(async function test_match_and_apply_all_variants() { + await assertActualEnginesEqualsExpected( + CONFIG, + { + locale: "en-US", + region: "CA", + }, + [ + { + identifier: "engine-1", + urls: { search: { params: [{ name: "partner-code", value: "foo" }] } }, + telemetrySuffix: "telemetry", + searchTermParamName: "search-param", + }, + ], + "Should match all variants and apply each variant property cumulatively." + ); +}); + +add_task(async function test_match_middle_variant() { + await assertActualEnginesEqualsExpected( + CONFIG, + { + locale: "en-US", + region: "US", + }, + [ + { + identifier: "engine-1", + urls: { search: { params: [{ name: "partner-code", value: "bar" }] } }, + telemetrySuffix: "telemetry", + }, + ], + "Should match first and second variants." + ); +}); + +add_task(async function test_match_first_and_last_variant() { + await assertActualEnginesEqualsExpected( + CONFIG, + { + locale: "en-GB", + region: "GB", + }, + [ + { + identifier: "engine-1", + urls: { search: { params: [{ name: "partner-code", value: "foo" }] } }, + searchTermParamName: "search-param", + }, + ], + "Should match first and last variant." + ); +}); + +add_task(async function test_match_variant_with_empty_params() { + await assertActualEnginesEqualsExpected( + CONFIG, + { + locale: "it", + region: "IT", + }, + [ + { + identifier: "engine-1", + urls: { search: { params: [] } }, + }, + ], + "Should match the first variant with empty params." + ); +}); + +add_task(async function test_config_has_not_been_modified() { + Assert.deepEqual( + CONFIG, + CONFIG_CLONE, + "Should not modify the original test config after applying variant engines to the base engine." + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_set_alias.js b/toolkit/components/search/tests/xpcshell/test_engine_set_alias.js new file mode 100644 index 0000000000..ec79fe6783 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_set_alias.js @@ -0,0 +1,132 @@ +"use strict"; + +add_setup(async function () { + useHttpServer(); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +}); + +add_task(async function test_engine_set_alias() { + info("Set engine alias"); + let extension = await SearchTestUtils.installSearchExtension( + { + name: "bacon", + keyword: "b", + search_url: "https://www.bacon.test/find", + }, + { skipUnload: true } + ); + let engine1 = await Services.search.getEngineByName("bacon"); + Assert.ok(engine1.aliases.includes("b")); + engine1.alias = "a"; + Assert.equal(engine1.alias, "a"); + await extension.unload(); +}); + +add_task(async function test_engine_set_alias_with_left_space() { + info("Set engine alias with left space"); + let extension = await SearchTestUtils.installSearchExtension( + { + name: "bacon", + keyword: " a", + search_url: "https://www.bacon.test/find", + }, + { skipUnload: true } + ); + let engine2 = await Services.search.getEngineByName("bacon"); + Assert.ok(engine2.aliases.includes("a")); + engine2.alias = " c"; + Assert.equal(engine2.alias, "c"); + await extension.unload(); +}); + +add_task(async function test_engine_set_alias_with_right_space() { + info("Set engine alias with right space"); + let extension = await SearchTestUtils.installSearchExtension( + { + name: "bacon", + keyword: "c ", + search_url: "https://www.bacon.test/find", + }, + { skipUnload: true } + ); + let engine3 = await Services.search.getEngineByName("bacon"); + Assert.ok(engine3.aliases.includes("c")); + engine3.alias = "o "; + Assert.equal(engine3.alias, "o"); + await extension.unload(); +}); + +add_task(async function test_engine_set_alias_with_right_left_space() { + info("Set engine alias with left and right space"); + let extension = await SearchTestUtils.installSearchExtension( + { + name: "bacon", + keyword: " o ", + search_url: "https://www.bacon.test/find", + }, + { skipUnload: true } + ); + let engine4 = await Services.search.getEngineByName("bacon"); + Assert.ok(engine4.aliases.includes("o")); + engine4.alias = " n "; + Assert.equal(engine4.alias, "n"); + await extension.unload(); +}); + +add_task(async function test_engine_set_alias_with_space() { + info("Set engine alias with space"); + let extension = await SearchTestUtils.installSearchExtension( + { + name: "bacon", + keyword: " ", + search_url: "https://www.bacon.test/find", + }, + { skipUnload: true } + ); + let engine5 = await Services.search.getEngineByName("bacon"); + Assert.equal(engine5.alias, ""); + engine5.alias = "b"; + Assert.equal(engine5.alias, "b"); + engine5.alias = " "; + Assert.equal(engine5.alias, ""); + await extension.unload(); +}); + +add_task(async function test_engine_change_alias() { + let extension = await SearchTestUtils.installSearchExtension( + { + name: "bacon", + keyword: " o ", + search_url: "https://www.bacon.test/find", + }, + { skipUnload: true } + ); + let engine6 = await Services.search.getEngineByName("bacon"); + + let promise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + + engine6.alias = "ba"; + + await promise; + Assert.equal( + engine6.alias, + "ba", + "Should have correctly notified and changed the alias." + ); + + let observed = false; + Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + observed = true; + }, SearchUtils.TOPIC_ENGINE_MODIFIED); + + engine6.alias = "ba"; + + Assert.equal(engine6.alias, "ba", "Should have not changed the alias"); + Assert.ok(!observed, "Should not have notified for no change in alias"); + + await extension.unload(); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_getSubmission_encoding.js b/toolkit/components/search/tests/xpcshell/test_getSubmission_encoding.js new file mode 100644 index 0000000000..c6f1099ddb --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_getSubmission_encoding.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const prefix = "https://example.com/?sourceId=Mozilla-search&search="; + +add_setup(async function () { + await SearchTestUtils.useTestEngines("simple-engines"); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +}); + +function testEncode(engine, charset, query, expected) { + engine.wrappedJSObject._queryCharset = charset; + + Assert.equal( + engine.getSubmission(query).uri.spec, + prefix + expected, + `Should have correctly encoded for ${charset}` + ); +} + +add_task(async function test_getSubmission_encoding() { + let engine = await Services.search.getEngineByName("Simple Engine"); + + testEncode(engine, "UTF-8", "caff\u00E8", "caff%C3%A8"); + testEncode(engine, "windows-1252", "caff\u00E8", "caff%E8"); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_getSubmission_params.js b/toolkit/components/search/tests/xpcshell/test_getSubmission_params.js new file mode 100644 index 0000000000..1be81a2e31 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_getSubmission_params.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + await SearchTestUtils.useTestEngines("simple-engines"); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +}); + +const searchTerms = "fxsearch"; +function checkSubstitution(url, prefix, engine, template, expected) { + url.template = prefix + template; + equal(engine.getSubmission(searchTerms).uri.spec, prefix + expected); +} + +add_task(async function test_paramSubstitution() { + let prefix = "https://example.com/?sourceId=Mozilla-search&search="; + let engine = await Services.search.getEngineByName("Simple Engine"); + let url = engine.wrappedJSObject._getURLOfType("text/html"); + equal(url.getSubmission("foo", engine).uri.spec, prefix + "foo"); + // Reset the engine parameters so we can have a clean template to use for + // the subsequent tests. + url.params = []; + + let check = checkSubstitution.bind(this, url, prefix, engine); + + // The same parameter can be used more than once. + check("{searchTerms}/{searchTerms}", searchTerms + "/" + searchTerms); + + // Optional parameters are replaced if we known them. + check("{searchTerms?}", searchTerms); + check("{unknownOptional?}", ""); + check("{unknownRequired}", "{unknownRequired}"); + + check("{language}", Services.locale.requestedLocale); + check("{language?}", Services.locale.requestedLocale); + + engine.wrappedJSObject._queryCharset = "UTF-8"; + check("{inputEncoding}", "UTF-8"); + check("{inputEncoding?}", "UTF-8"); + check("{outputEncoding}", "UTF-8"); + check("{outputEncoding?}", "UTF-8"); + + // 'Unsupported' parameters with hard coded values used only when the parameter is required. + check("{count}", "20"); + check("{count?}", ""); + check("{startIndex}", "1"); + check("{startIndex?}", ""); + check("{startPage}", "1"); + check("{startPage?}", ""); + + check("{moz:locale}", Services.locale.requestedLocale); +}); + +add_task(async function test_mozParamsFailForNonAppProvided() { + await SearchTestUtils.installSearchExtension(); + + let prefix = "https://example.com/?q="; + let engine = await Services.search.getEngineByName("Example"); + let url = engine.wrappedJSObject._getURLOfType("text/html"); + equal(url.getSubmission("foo", engine).uri.spec, prefix + "foo"); + // Reset the engine parameters so we can have a clean template to use for + // the subsequent tests. + url.params = []; + + let check = checkSubstitution.bind(this, url, prefix, engine); + + // Test moz: parameters (only supported for built-in engines, ie _isDefault == true). + check("{moz:locale}", "{moz:locale}"); + + await promiseAfterSettings(); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_getSubmission_params_pref.js b/toolkit/components/search/tests/xpcshell/test_getSubmission_params_pref.js new file mode 100644 index 0000000000..5283207394 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_getSubmission_params_pref.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Test that MozParam condition="pref" values used in search URLs are from the + * default branch, and that their special characters are URL encoded. */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const defaultBranch = Services.prefs.getDefaultBranch( + SearchUtils.BROWSER_SEARCH_PREF +); +const baseURL = "https://www.google.com/search?q=foo"; + +add_setup(async function () { + // The test engines used in this test need to be recognized as 'default' + // engines, or their MozParams will be ignored. + await SearchTestUtils.useTestEngines(); +}); + +add_task(async function test_pref_initial_value() { + defaultBranch.setCharPref("param.code", "good&id=unique"); + + // Preference params are only allowed to be modified on the user branch + // on nightly builds. For non-nightly builds, check that modifying on the + // normal branch doesn't work. + if (!AppConstants.NIGHTLY_BUILD) { + Services.prefs.setCharPref( + SearchUtils.BROWSER_SEARCH_PREF + "param.code", + "bad" + ); + } + + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); + + const engine = Services.search.getEngineByName("engine-pref"); + Assert.equal( + engine.getSubmission("foo").uri.spec, + baseURL + "&code=good%26id%3Dunique", + "Should have got the submission URL with the correct code" + ); + + // Now clear the user-set preference. Having a user set preference means + // we don't get updates from the pref service of changes on the default + // branch. Normally, this won't be an issue, since we don't expect users + // to be playing with these prefs, and worst-case, they'll just get the + // actual change on restart. + Services.prefs.clearUserPref(SearchUtils.BROWSER_SEARCH_PREF + "param.code"); +}); + +add_task(async function test_pref_updated() { + // Update the pref without re-init nor restart. + defaultBranch.setCharPref("param.code", "supergood&id=unique123456"); + + const engine = Services.search.getEngineByName("engine-pref"); + Assert.equal( + engine.getSubmission("foo").uri.spec, + baseURL + "&code=supergood%26id%3Dunique123456", + "Should have got the submission URL with the updated code" + ); +}); + +add_task(async function test_pref_cleared() { + // Update the pref without re-init nor restart. + // Note you can't delete a preference from the default branch. + defaultBranch.setCharPref("param.code", ""); + + let engine = Services.search.getEngineByName("engine-pref"); + Assert.equal( + engine.getSubmission("foo").uri.spec, + baseURL, + "Should have just the base URL after the pref was cleared" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_getSubmission_params_prefNimbus.js b/toolkit/components/search/tests/xpcshell/test_getSubmission_params_prefNimbus.js new file mode 100644 index 0000000000..e9b55ff3dc --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_getSubmission_params_prefNimbus.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Test that MozParam condition="pref" values used in search URLs can be set + by Nimbus, and that their special characters are URL encoded. */ + +"use strict"; + +const { NimbusFeatures } = ChromeUtils.importESModule( + "resource://nimbus/ExperimentAPI.sys.mjs" +); + +const baseURL = "https://www.google.com/search?q=foo"; + +let getVariableStub; +let updateStub; + +add_setup(async function () { + updateStub = sinon.stub(NimbusFeatures.search, "onUpdate"); + getVariableStub = sinon.stub(NimbusFeatures.search, "getVariable"); + sinon.stub(NimbusFeatures.search, "ready").resolves(); + + // The test engines used in this test need to be recognized as 'default' + // engines, or their MozParams will be ignored. + await SearchTestUtils.useTestEngines(); +}); + +add_task(async function test_pref_initial_value() { + // These values should match the nimbusParams below and the data/test/manifest.json + // search engine configuration + getVariableStub.withArgs("extraParams").returns([ + { + key: "code", + // The & and = in this parameter are to check that they are correctly + // encoded, and not treated as a separate parameter. + value: "good&id=unique", + }, + ]); + + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); + + Assert.ok( + updateStub.called, + "Should have called onUpdate to listen for future updates" + ); + + const engine = Services.search.getEngineByName("engine-pref"); + Assert.equal( + engine.getSubmission("foo").uri.spec, + baseURL + "&code=good%26id%3Dunique", + "Should have got the submission URL with the correct code" + ); +}); + +add_task(async function test_pref_updated() { + getVariableStub.withArgs("extraParams").returns([ + { + key: "code", + // The & and = in this parameter are to check that they are correctly + // encoded, and not treated as a separate parameter. + value: "supergood&id=unique123456", + }, + ]); + // Update the pref without re-init nor restart. + updateStub.firstCall.args[0](); + + const engine = Services.search.getEngineByName("engine-pref"); + Assert.equal( + engine.getSubmission("foo").uri.spec, + baseURL + "&code=supergood%26id%3Dunique123456", + "Should have got the submission URL with the updated code" + ); +}); + +add_task(async function test_multiple_params() { + getVariableStub.withArgs("extraParams").returns([ + { + key: "code", + value: "sng", + }, + { + key: "test", + value: "sup", + }, + ]); + // Update the pref without re-init nor restart. + updateStub.firstCall.args[0](); + + let engine = Services.search.getEngineByName("engine-pref"); + Assert.equal( + engine.getSubmission("foo").uri.spec, + baseURL + "&code=sng&test=sup", + "Should have got the submission URL with both parameters" + ); + + // Test removing just one of the parameters. + getVariableStub.withArgs("extraParams").returns([ + { + key: "code", + value: "sng", + }, + ]); + // Update the pref without re-init nor restart. + updateStub.firstCall.args[0](); + + engine = Services.search.getEngineByName("engine-pref"); + Assert.equal( + engine.getSubmission("foo").uri.spec, + baseURL + "&code=sng", + "Should have got the submission URL with one parameter" + ); +}); + +add_task(async function test_pref_cleared() { + // Update the pref without re-init nor restart. + // Note you can't delete a preference from the default branch. + getVariableStub.withArgs("extraParams").returns([]); + updateStub.firstCall.args[0](); + + let engine = Services.search.getEngineByName("engine-pref"); + Assert.equal( + engine.getSubmission("foo").uri.spec, + baseURL, + "Should have just the base URL after the pref was cleared" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_getSubmission_params_prefNimbus_invalid.js b/toolkit/components/search/tests/xpcshell/test_getSubmission_params_prefNimbus_invalid.js new file mode 100644 index 0000000000..e519a74c64 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_getSubmission_params_prefNimbus_invalid.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that if Nimbus provides invalid values for extraParams, the search + * service can still initialize and run. + * This is separate to test_getSubmission_params_prefNimbus so that initialization + * can be properly tested. + */ + +"use strict"; + +const { NimbusFeatures } = ChromeUtils.importESModule( + "resource://nimbus/ExperimentAPI.sys.mjs" +); + +const baseURL = "https://www.google.com/search?q=foo"; + +let getVariableStub; +let updateStub; + +add_setup(async function () { + consoleAllowList.push("Failed to load nimbus variables for extraParams"); + + updateStub = sinon.stub(NimbusFeatures.search, "onUpdate"); + getVariableStub = sinon.stub(NimbusFeatures.search, "getVariable"); + sinon.stub(NimbusFeatures.search, "ready").resolves(); + + // The test engines used in this test need to be recognized as 'default' + // engines, or their MozParams will be ignored. + await SearchTestUtils.useTestEngines(); +}); + +add_task(async function test_bad_nimbus_setting_on_init() { + getVariableStub.withArgs("extraParams").returns({ foo: "bar" }); + + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); + + Assert.ok( + updateStub.called, + "Should have called onUpdate to listen for future updates" + ); + + const engine = Services.search.getEngineByName("engine-pref"); + Assert.equal( + engine.getSubmission("foo").uri.spec, + baseURL, + "Should have been able to get a submission URL" + ); +}); + +add_task(async function test_switch_to_good_nimbus_setting() { + // Switching to a good structure should provide the parameter. + getVariableStub.withArgs("extraParams").returns([ + { + key: "code", + // The & and = in this parameter are to check that they are correctly + // encoded, and not treated as a separate parameter. + value: "supergood&id=unique123456", + }, + ]); + + updateStub.firstCall.args[0](); + + const engine = Services.search.getEngineByName("engine-pref"); + Assert.equal( + engine.getSubmission("foo").uri.spec, + baseURL + "&code=supergood%26id%3Dunique123456", + "Should have got the submission URL with the updated code" + ); +}); + +add_task(async function test_switch_back_to_bad_nimbus_setting() { + // Switching from a valid Nimbus setting to an invalid setting should fallback + // to a valid submission URL. + getVariableStub.withArgs("extraParams").returns({ bar: "foo" }); + + updateStub.firstCall.args[0](); + + const engine = Services.search.getEngineByName("engine-pref"); + Assert.equal( + engine.getSubmission("foo").uri.spec, + baseURL, + "Should have not have the extra parameters on the submission URL after a bad update" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_getSubmission_params_purpose.js b/toolkit/components/search/tests/xpcshell/test_getSubmission_params_purpose.js new file mode 100644 index 0000000000..35a33280f6 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_getSubmission_params_purpose.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test that a search purpose can be specified and that query parameters for + * that purpose are included in the search URL. + */ + +"use strict"; + +add_setup(async function () { + // The test engines used in this test need to be recognized as 'default' + // engines, or their MozParams used to set the purpose will be ignored. + await SearchTestUtils.useTestEngines(); + + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +}); + +add_task(async function test_purpose() { + let engine = Services.search.getEngineByName("Test search engine"); + + function check_submission(aValue, aSearchTerm, aType, aPurpose) { + let submissionURL = engine.getSubmission(aSearchTerm, aType, aPurpose).uri + .spec; + let searchParams = new URLSearchParams(submissionURL.split("?")[1]); + if (aValue) { + Assert.equal(searchParams.get("channel"), aValue); + } else { + Assert.ok(!searchParams.has("channel")); + } + Assert.equal(searchParams.get("q"), aSearchTerm); + } + + check_submission("", "foo"); + check_submission("", "foo", null); + check_submission("", "foo", "text/html"); + check_submission("rcs", "foo", null, "contextmenu"); + check_submission("rcs", "foo", "text/html", "contextmenu"); + check_submission("fflb", "foo", null, "keyword"); + check_submission("fflb", "foo", "text/html", "keyword"); + check_submission("", "foo", "text/html", "invalid"); + + // Tests for a purpose on the search form (ie. empty query). + engine = Services.search.getEngineByName("engine-rel-searchform-purpose"); + + // See bug 1485508 + Assert.ok(!engine.searchForm.includes("?&")); + + // verify that the 'system' purpose falls back to the 'searchbar' purpose. + check_submission("sb", "foo", "text/html", "system"); + check_submission("sb", "foo", "text/html", "searchbar"); +}); + +add_task(async function test_purpose() { + let engine = Services.search.getEngineByName( + "Test search engine (Reordered)" + ); + + function check_submission(aValue, aSearchTerm, aType, aPurpose) { + let submissionURL = engine.getSubmission(aSearchTerm, aType, aPurpose).uri + .spec; + let searchParams = new URLSearchParams(submissionURL.split("?")[1]); + if (aValue) { + Assert.equal(searchParams.get("channel"), aValue); + } else { + Assert.ok(!searchParams.has("channel")); + } + Assert.equal(searchParams.get("q"), aSearchTerm); + } + + check_submission("", "foo"); + check_submission("", "foo", null); + check_submission("", "foo", "text/html"); + check_submission("rcs", "foo", null, "contextmenu"); + check_submission("rcs", "foo", "text/html", "contextmenu"); + check_submission("fflb", "foo", null, "keyword"); + check_submission("fflb", "foo", "text/html", "keyword"); + check_submission("", "foo", "text/html", "invalid"); + + // See bug 1485508 + Assert.ok(!engine.searchForm.includes("?&")); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_identifiers.js b/toolkit/components/search/tests/xpcshell/test_identifiers.js new file mode 100644 index 0000000000..4e39507d3a --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_identifiers.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test of a search engine's telemetryId. + */ + +"use strict"; + +add_setup(async function () { + await SearchTestUtils.useTestEngines("simple-engines"); + await AddonTestUtils.promiseStartupManager(); + + const result = await Services.search.init(); + Assert.ok( + Components.isSuccessCode(result), + "Should have initialized the service" + ); + + useHttpServer(); + await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engine.xml`, + }); +}); + +function checkIdentifier(engineName, expectedIdentifier, expectedTelemetryId) { + const engine = Services.search.getEngineByName(engineName); + Assert.ok( + engine instanceof Ci.nsISearchEngine, + "Should be derived from nsISearchEngine" + ); + + Assert.equal( + engine.telemetryId, + expectedTelemetryId, + "Should have the correct telemetry Id" + ); + + // TODO: Bug 1877721 - We have 3 forms of identifiers which causes confusion, + // we can remove the identifier for nsISearchEngine. + Assert.equal( + engine.identifier, + expectedIdentifier, + "Should have the correct identifier" + ); +} + +add_task(async function test_from_profile() { + // An engine loaded from the profile directory won't have an identifier, + // because it's not built-in. + checkIdentifier(kTestEngineName, null, `other-${kTestEngineName}`); +}); + +add_task(async function test_from_telemetry_id() { + checkIdentifier("basic", "basic-telemetry", "basic-telemetry"); +}); + +add_task(async function test_from_webextension_id() { + // If not specified, the telemetry Id is derived from the WebExtension prefix, + // it should not use the WebExtension display name. + checkIdentifier("Simple Engine", "simple", "simple"); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_ignorelist.js b/toolkit/components/search/tests/xpcshell/test_ignorelist.js new file mode 100644 index 0000000000..cd3a3a7238 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_ignorelist.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const kSearchEngineID1 = "ignorelist_test_engine1"; +const kSearchEngineID2 = "ignorelist_test_engine2"; +const kSearchEngineID3 = "ignorelist_test_engine3"; +const kSearchEngineURL1 = + "https://example.com/?search={searchTerms}&ignore=true"; +const kSearchEngineURL2 = + "https://example.com/?search={searchTerms}&IGNORE=TRUE"; +const kSearchEngineURL3 = "https://example.com/?search={searchTerms}"; +const kExtensionID = "searchignore@mozilla.com"; + +add_setup(async function () { + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_ignoreList() { + await setupRemoteSettings(); + + Assert.ok( + !Services.search.isInitialized, + "Search service should not be initialized to begin with." + ); + + let updatePromise = SearchTestUtils.promiseSearchNotification( + "settings-update-complete" + ); + + await SearchTestUtils.installSearchExtension({ + name: kSearchEngineID1, + search_url: kSearchEngineURL1, + }); + + await updatePromise; + + let engine = Services.search.getEngineByName(kSearchEngineID1); + Assert.equal( + engine, + null, + "Engine with ignored search params should not exist" + ); + + await SearchTestUtils.installSearchExtension({ + name: kSearchEngineID2, + search_url: kSearchEngineURL2, + }); + + // An ignored engine shouldn't be available at all + engine = Services.search.getEngineByName(kSearchEngineID2); + Assert.equal( + engine, + null, + "Engine with ignored search params of a different case should not exist" + ); + + await SearchTestUtils.installSearchExtension({ + id: kExtensionID, + name: kSearchEngineID3, + search_url: kSearchEngineURL3, + }); + + // An ignored engine shouldn't be available at all + engine = Services.search.getEngineByName(kSearchEngineID3); + Assert.equal( + engine, + null, + "Engine with ignored extension id should not exist" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_ignorelist_update.js b/toolkit/components/search/tests/xpcshell/test_ignorelist_update.js new file mode 100644 index 0000000000..fc090d6ff7 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_ignorelist_update.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const kSearchEngineID1 = "ignorelist_test_engine1"; +const kSearchEngineID2 = "ignorelist_test_engine2"; +const kSearchEngineID3 = "ignorelist_test_engine3"; +const kSearchEngineURL1 = + "https://example.com/?search={searchTerms}&ignore=true"; +const kSearchEngineURL2 = + "https://example.com/?search={searchTerms}&IGNORE=TRUE"; +const kSearchEngineURL3 = "https://example.com/?search={searchTerms}"; +const kExtensionID = "searchignore@mozilla.com"; + +add_setup(async function () { + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_ignoreList() { + Assert.ok( + !Services.search.isInitialized, + "Search service should not be initialized to begin with." + ); + + let updatePromise = SearchTestUtils.promiseSearchNotification( + "settings-update-complete" + ); + await SearchTestUtils.installSearchExtension({ + name: kSearchEngineID1, + search_url: kSearchEngineURL1, + }); + await SearchTestUtils.installSearchExtension({ + name: kSearchEngineID2, + search_url: kSearchEngineURL2, + }); + await SearchTestUtils.installSearchExtension({ + id: kExtensionID, + name: kSearchEngineID3, + search_url: kSearchEngineURL3, + }); + + // Ensure that the initial remote settings update from default values is + // complete. The defaults do not include the special inclusions inserted below. + await updatePromise; + + for (let engineName of [ + kSearchEngineID1, + kSearchEngineID2, + kSearchEngineID3, + ]) { + Assert.ok( + await Services.search.getEngineByName(engineName), + `Engine ${engineName} should be present` + ); + } + + // Simulate an ignore list update. + await RemoteSettings("hijack-blocklists").emit("sync", { + data: { + current: [ + { + id: "load-paths", + schema: 1553857697843, + last_modified: 1553859483588, + matches: ["[addon]searchignore@mozilla.com"], + }, + { + id: "submission-urls", + schema: 1553857697843, + last_modified: 1553859435500, + matches: ["ignore=true"], + }, + ], + }, + }); + + for (let engineName of [ + kSearchEngineID1, + kSearchEngineID2, + kSearchEngineID3, + ]) { + Assert.equal( + await Services.search.getEngineByName(engineName), + null, + `Engine ${engineName} should not be present` + ); + } +}); diff --git a/toolkit/components/search/tests/xpcshell/test_initialization.js b/toolkit/components/search/tests/xpcshell/test_initialization.js new file mode 100644 index 0000000000..c89f3cfcb3 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_initialization.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that a delayed add-on manager start up does not affect the start up +// of the search service. + +"use strict"; + +const CONFIG = [ + { + webExtension: { + id: "engine@search.mozilla.org", + name: "Test search engine", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "channel", + condition: "purpose", + purpose: "contextmenu", + value: "rcs", + }, + { + name: "channel", + condition: "purpose", + purpose: "keyword", + value: "fflb", + }, + ], + suggest_url: + "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}", + }, + orderHint: 30, + appliesTo: [ + { + included: { everywhere: true }, + default: "yes", + }, + ], + }, +]; + +add_setup(() => { + do_get_profile(); + Services.fog.initializeFOG(); +}); + +add_task(async function test_initialization_delayed_addon_manager() { + let stub = await SearchTestUtils.useTestEngines("data", null, CONFIG); + // Wait until the search service gets its configuration before starting + // to initialise the add-on manager. This simulates the add-on manager + // starting late which used to cause the search service to fail to load any + // engines. + stub.callsFake(() => { + Services.tm.dispatchToMainThread(() => { + AddonTestUtils.promiseStartupManager(); + }); + return CONFIG; + }); + + await Services.search.init(); + + Assert.equal( + Services.search.defaultEngine.name, + "Test search engine", + "Test engine shouldn't be the default anymore" + ); + + await assertGleanDefaultEngine({ + normal: { + engineId: "engine", + displayName: "Test search engine", + loadPath: [ + SearchUtils.newSearchConfigEnabled + ? "[app]engine@search.mozilla.org" + : "[addon]engine@search.mozilla.org", + ], + submissionUrl: "https://www.google.com/search?q=", + verified: "default", + }, + }); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_initialization_status_telemetry.js b/toolkit/components/search/tests/xpcshell/test_initialization_status_telemetry.js new file mode 100644 index 0000000000..fc62ded787 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_initialization_status_telemetry.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests telemetry is captured when search service initialization has failed or + * succeeded. + */ +const searchService = Services.search.wrappedJSObject; + +add_setup(async () => { + consoleAllowList.push("#init: failure initializing search:"); + await SearchTestUtils.useTestEngines("simple-engines"); + await AddonTestUtils.promiseStartupManager(); + Services.fog.initializeFOG(); +}); + +add_task(async function test_init_success_telemetry() { + Assert.equal( + searchService.isInitialized, + false, + "Search Service should not be initialized." + ); + + await Services.search.init(); + + Assert.equal( + searchService.hasSuccessfullyInitialized, + true, + "Search Service should have initialized successfully." + ); + + Assert.equal( + 1, + await Glean.searchService.initializationStatus.success.testGetValue(), + "Should have incremented init success by one." + ); +}); + +add_task(async function test_init_failure_telemetry() { + await startInitFailure("Settings"); + Assert.equal( + 1, + await Glean.searchService.initializationStatus.failedSettings.testGetValue(), + "Should have incremented get settings failure by one." + ); + + await startInitFailure("FetchEngines"); + Assert.equal( + 1, + await Glean.searchService.initializationStatus.failedFetchEngines.testGetValue(), + "Should have incremented fetch engines failure by one." + ); + + await startInitFailure("LoadEngines"); + Assert.equal( + 1, + await Glean.searchService.initializationStatus.failedLoadEngines.testGetValue(), + "Should have incremented load engines failure by one." + ); +}); + +async function startInitFailure(errorType) { + searchService.reset(); + searchService.errorToThrowInTest = errorType; + + Assert.equal( + searchService.isInitialized, + false, + "Search Service should not be initialized." + ); + + let regex = new RegExp( + `Fake ${errorType} error during search service initialization.` + ); + + await Assert.rejects( + Services.search.init(), + regex, + "Should have thrown an error on init." + ); + + await Assert.rejects( + Services.search.promiseInitialized, + regex, + "Should have rejected the promise." + ); + + searchService.errorToThrowInTest = null; +} diff --git a/toolkit/components/search/tests/xpcshell/test_initialization_with_region.js b/toolkit/components/search/tests/xpcshell/test_initialization_with_region.js new file mode 100644 index 0000000000..fb9cee5124 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_initialization_with_region.js @@ -0,0 +1,170 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SEARCH_SERVICE_TOPIC = "browser-search-service"; +const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified"; + +const CONFIG = [ + { + webExtension: { + id: "engine@search.mozilla.org", + name: "Test search engine", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "channel", + condition: "purpose", + purpose: "contextmenu", + value: "rcs", + }, + { + name: "channel", + condition: "purpose", + purpose: "keyword", + value: "fflb", + }, + ], + suggest_url: + "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}", + }, + orderHint: 30, + appliesTo: [ + { + included: { everywhere: true }, + excluded: { regions: ["FR"] }, + default: "yes", + defaultPrivate: "yes", + }, + ], + }, + { + webExtension: { + id: "engine-pref@search.mozilla.org", + name: "engine-pref", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "code", + condition: "pref", + pref: "code", + }, + { + name: "test", + condition: "pref", + pref: "test", + }, + ], + }, + orderHint: 20, + appliesTo: [ + { + included: { regions: ["FR"] }, + default: "yes", + defaultPrivate: "yes", + }, + ], + }, +]; + +// Default engine with no region defined. +const DEFAULT = "Test search engine"; +// Default engine with region set to FR. +const FR_DEFAULT = "engine-pref"; + +function listenFor(name, key) { + let notifyObserved = false; + let obs = (subject, topic, data) => { + if (data == key) { + notifyObserved = true; + } + }; + Services.obs.addObserver(obs, name); + + return () => { + Services.obs.removeObserver(obs, name); + return notifyObserved; + }; +} + +add_setup(async function () { + Services.prefs.setBoolPref("browser.search.separatePrivateDefault", true); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + true + ); + + SearchTestUtils.useMockIdleService(); + await SearchTestUtils.useTestEngines("data", null, CONFIG); + await AddonTestUtils.promiseStartupManager(); +}); + +// This tests what we expect is the normal startup route for a fresh profile - +// the search service initializes with no region details, then gets a region +// notified part way through / afterwards. +add_task(async function test_initialization_with_region() { + let reloadObserved = listenFor(SEARCH_SERVICE_TOPIC, "engines-reloaded"); + let initPromise; + + // Ensure the region lookup completes after init so the + // engines are reloaded + let srv = useHttpServer(); + srv.registerPathHandler("/fetch_region", async (req, res) => { + res.processAsync(); + await initPromise; + res.setStatusLine("1.1", 200, "OK"); + res.write(JSON.stringify({ country_code: "FR" })); + res.finish(); + }); + + Services.prefs.setCharPref( + "browser.region.network.url", + `http://localhost:${srv.identity.primaryPort}/fetch_region` + ); + + Region._setHomeRegion("", false); + Region.init(); + + initPromise = Services.search.init(); + await initPromise; + + let otherPromises = [ + // This test expects settings to be saved twice. + promiseAfterSettings().then(promiseAfterSettings), + SearchTestUtils.promiseSearchNotification( + "engine-default", + SEARCH_ENGINE_TOPIC + ), + ]; + + Assert.equal( + Services.search.defaultEngine.name, + DEFAULT, + "Test engine shouldn't be the default anymore" + ); + + await Promise.all(otherPromises); + + // Ensure that correct engine is being reported as the default. + Assert.equal( + Services.search.defaultEngine.name, + FR_DEFAULT, + "engine-pref should be the default in FR" + ); + Assert.equal( + (await Services.search.getDefaultPrivate()).name, + FR_DEFAULT, + "engine-pref should be the private default in FR" + ); + + Assert.ok(reloadObserved(), "Engines do reload with delayed region fetch"); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_list_json_locale.js b/toolkit/components/search/tests/xpcshell/test_list_json_locale.js new file mode 100644 index 0000000000..75c77be22a --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_list_json_locale.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Check default search engine is picked from list.json searchDefault */ + +"use strict"; + +add_setup(async function () { + await SearchTestUtils.useTestEngines(); + + Services.locale.availableLocales = [ + ...Services.locale.availableLocales, + "de", + "fr", + ]; + + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + true + ); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); + Region._setHomeRegion("US", false); +}); + +add_task(async function test_listJSONlocale() { + Services.locale.requestedLocales = ["de"]; + + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); + + Assert.ok(Services.search.isInitialized, "search initialized"); + + let sortedEngines = await Services.search.getEngines(); + Assert.equal(sortedEngines.length, 1, "Should have only one engine"); + + Assert.equal( + Services.search.defaultEngine.name, + "Test search engine", + "Should have the correct default engine" + ); + Assert.equal( + Services.search.defaultPrivateEngine.name, + // 'de' only displays google, so we'll be using the same engine as the + // normal default. + "Test search engine", + "Should have the correct private default engine" + ); +}); + +// Check that switching locale switches search engines +add_task(async function test_listJSONlocaleSwitch() { + await promiseSetLocale("fr"); + + Assert.ok(Services.search.isInitialized, "search initialized"); + + let sortedEngines = await Services.search.getEngines(); + Assert.deepEqual( + sortedEngines.map(e => e.name), + ["Test search engine", "engine-pref", "engine-resourceicon"], + "Should have the correct engine list" + ); + + Assert.equal( + Services.search.defaultEngine.name, + "Test search engine", + "Should have the correct default engine" + ); + Assert.equal( + Services.search.defaultPrivateEngine.name, + "engine-pref", + "Should have the correct private default engine" + ); +}); + +// Check that region overrides apply +add_task(async function test_listJSONRegionOverride() { + await promiseSetHomeRegion("RU"); + + Assert.ok(Services.search.isInitialized, "search initialized"); + + let sortedEngines = await Services.search.getEngines(); + Assert.deepEqual( + sortedEngines.map(e => e.name), + ["Test search engine", "engine-pref", "engine-chromeicon"], + "Should have the correct engine list" + ); + + Assert.equal( + Services.search.defaultEngine.name, + "Test search engine", + "Should have the correct default engine" + ); + Assert.equal( + Services.search.defaultPrivateEngine.name, + "engine-pref", + "Should have the correct private default engine" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_list_json_no_private_default.js b/toolkit/components/search/tests/xpcshell/test_list_json_no_private_default.js new file mode 100644 index 0000000000..8de8ad6a34 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_list_json_no_private_default.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// TODO: Test fallback to normal default when no private set at all. + +/* Check default search engine is picked from list.json searchDefault */ + +"use strict"; + +// Check that current engine matches with US searchDefault from list.json +add_task(async function test_searchDefaultEngineUS() { + await SearchTestUtils.useTestEngines("data1"); + + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + true + ); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); + Services.prefs.setCharPref(SearchUtils.BROWSER_SEARCH_PREF + "region", "US"); + + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); + + Assert.ok(Services.search.isInitialized, "search initialized"); + + Assert.equal( + Services.search.appDefaultEngine.name, + "engine1", + "Should have the expected engine as app default" + ); + Assert.equal( + Services.search.defaultEngine.name, + "engine1", + "Should have the expected engine as default" + ); + Assert.equal( + Services.search.appPrivateDefaultEngine.name, + "engine1", + "Should have the same engine for the app private default" + ); + Assert.equal( + Services.search.defaultPrivateEngine.name, + "engine1", + "Should have the same engine for the private default" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_list_json_searchdefault.js b/toolkit/components/search/tests/xpcshell/test_list_json_searchdefault.js new file mode 100644 index 0000000000..38c7f302d2 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_list_json_searchdefault.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Check default search engine is picked from list.json searchDefault */ + +"use strict"; + +// Check that current engine matches with US searchDefault from list.json +add_task(async function test_searchDefaultEngineUS() { + await SearchTestUtils.useTestEngines(); + + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + true + ); + + Services.prefs.setCharPref(SearchUtils.BROWSER_SEARCH_PREF + "region", "US"); + + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); + + Assert.ok(Services.search.isInitialized, "search initialized"); + + Assert.equal( + Services.search.defaultEngine.name, + "Test search engine", + "Should have the expected engine as default." + ); + Assert.equal( + Services.search.appDefaultEngine.name, + "Test search engine", + "Should have the expected engine as the app default" + ); + + // First with the pref off to check using the existing values. + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + false + ); + + Assert.equal( + Services.search.defaultPrivateEngine.name, + Services.search.defaultEngine.name, + "Should have the normal default engine when separate private browsing is off." + ); + Assert.equal( + Services.search.appPrivateDefaultEngine.name, + Services.search.appDefaultEngine.name, + "Should have the normal app engine when separate private browsing is off." + ); + + // Then with the pref on. + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); + + Assert.equal( + Services.search.defaultPrivateEngine.name, + "engine-pref", + "Should have the private default engine when separate private browsing is on." + ); + Assert.equal( + Services.search.appPrivateDefaultEngine.name, + "engine-pref", + "Should have the app private engine set correctly when separate private browsing is on." + ); + + Services.prefs.clearUserPref("browser.search.region"); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_list_json_searchorder.js b/toolkit/components/search/tests/xpcshell/test_list_json_searchorder.js new file mode 100644 index 0000000000..a4933657be --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_list_json_searchorder.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Check default search engine is picked from list.json searchDefault */ + +"use strict"; + +add_setup(async function () { + await AddonTestUtils.promiseStartupManager(); + + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + true + ); + + await SearchTestUtils.useTestEngines(); + await Services.search.init(); +}); + +async function checkOrder(expectedOrder) { + const sortedEngines = await Services.search.getEngines(); + Assert.deepEqual( + sortedEngines.map(s => s.name), + expectedOrder, + "Should have the expected engine order" + ); +} + +add_task(async function test_searchOrderJSON_no_separate_private() { + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + false + ); + + await checkOrder([ + // Default engine + "Test search engine", + // Two engines listed in searchOrder. + "engine-resourceicon", + "engine-chromeicon", + // Rest of the engines in order. + "engine-pref", + "engine-rel-searchform-purpose", + "Test search engine (Reordered)", + ]); +}); + +add_task(async function test_searchOrderJSON_separate_private() { + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); + + await checkOrder([ + // Default engine + "Test search engine", + // Default private engine + "engine-pref", + // Two engines listed in searchOrder. + "engine-resourceicon", + "engine-chromeicon", + // Rest of the engines in order. + "engine-rel-searchform-purpose", + "Test search engine (Reordered)", + ]); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_location_timeout_xhr.js b/toolkit/components/search/tests/xpcshell/test_location_timeout_xhr.js new file mode 100644 index 0000000000..9f965271c2 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_location_timeout_xhr.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This is testing the long, last-resort XHR-based timeout for the location +// search. + +function startServer(continuePromise) { + let srv = new HttpServer(); + function lookupCountry(metadata, response) { + response.processAsync(); + // wait for our continuePromise to resolve before writing a valid + // response. + // This will be resolved after the timeout period, so we can check + // the behaviour in that case. + continuePromise.then(() => { + response.setStatusLine("1.1", 200, "OK"); + response.write('{"country_code" : "AU"}'); + response.finish(); + }); + } + srv.registerPathHandler("/lookup_country", lookupCountry); + srv.start(-1); + return srv; +} + +function verifyProbeSum(probe, sum) { + let histogram = Services.telemetry.getHistogramById(probe); + let snapshot = histogram.snapshot(); + equal(snapshot.sum, sum, probe); +} + +add_setup(async function () { + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_location_timeout_xhr() { + let resolveContinuePromise; + let continuePromise = new Promise(resolve => { + resolveContinuePromise = resolve; + }); + + let server = startServer(continuePromise); + let url = + "http://localhost:" + server.identity.primaryPort + "/lookup_country"; + Services.prefs.setCharPref("browser.search.geoip.url", url); + // The timeout for the timer. + Services.prefs.setIntPref("browser.search.geoip.timeout", 10); + let promiseXHRStarted = SearchTestUtils.promiseSearchNotification( + "geoip-lookup-xhr-starting" + ); + await Services.search.init(); + ok( + !Services.prefs.prefHasUserValue("browser.search.region"), + "should be no region pref" + ); + // should be no result recorded at all. + checkCountryResultTelemetry(null); + + // should not have SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS recorded as our + // test server is still blocked on our promise. + verifyProbeSum("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS", 0); + + promiseXHRStarted.then(xhr => { + // Set the timeout on the xhr object to an extremely low value, so it + // should timeout immediately. + xhr.timeout = 10; + // wait for the xhr timeout to fire. + SearchTestUtils.promiseSearchNotification("geoip-lookup-xhr-complete").then( + () => { + // should have the XHR timeout recorded. + checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.TIMEOUT); + // still should not have a report of how long the response took as we + // only record that on success responses. + verifyProbeSum("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS", 0); + // and we still don't know the country code or region. + ok( + !Services.prefs.prefHasUserValue("browser.search.region"), + "should be no region pref" + ); + + // unblock the server even though nothing is listening. + resolveContinuePromise(); + + return new Promise(resolve => { + server.stop(resolve); + }); + } + ); + }); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_maybereloadengine_order.js b/toolkit/components/search/tests/xpcshell/test_maybereloadengine_order.js new file mode 100644 index 0000000000..663b7205a7 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_maybereloadengine_order.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_CONFIG = [ + { + webExtension: { + id: "plainengine@search.mozilla.org", + name: "Plain", + search_url: "https://duckduckgo.com/", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + ], + suggest_url: "https://ac.duckduckgo.com/ac/q={searchTerms}&type=list", + }, + appliesTo: [{ included: { everywhere: true } }], + }, + { + webExtension: { + id: "special-engine@search.mozilla.org", + name: "Special", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "client", + condition: "purpose", + purpose: "keyword", + value: "firefox-b-1-ab", + }, + { + name: "client", + condition: "purpose", + purpose: "searchbar", + value: "firefox-b-1", + }, + ], + suggest_url: + "https://www.google.com/complete/search?client=firefox&q={searchTerms}", + }, + appliesTo: [{ default: "yes", included: { regions: ["FR"] } }], + }, +]; + +add_setup(async function () { + await SearchTestUtils.useTestEngines("test-extensions", null, TEST_CONFIG); + await AddonTestUtils.promiseStartupManager(); + + registerCleanupFunction(AddonTestUtils.promiseShutdownManager); +}); + +add_task(async function basic_multilocale_test() { + let resolver; + let initPromise = new Promise(resolve => (resolver = resolve)); + useCustomGeoServer("FR", initPromise); + + await Services.search.init(); + await Services.search.getAppProvidedEngines(); + resolver(); + await SearchTestUtils.promiseSearchNotification("engines-reloaded"); + + let engines = await Services.search.getAppProvidedEngines(); + + Assert.deepEqual( + engines.map(e => e._name), + ["Special", "Plain"], + "Special engine is default so should be first" + ); + + engines.forEach(engine => { + Assert.ok(!engine._metaData.order, "Order is not defined"); + }); + + Assert.equal( + Services.search.wrappedJSObject._settings.getMetaDataAttribute( + "useSavedOrder" + ), + false, + "We should not set the engine order during maybeReloadEngines" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_migrateWebExtensionEngine.js b/toolkit/components/search/tests/xpcshell/test_migrateWebExtensionEngine.js new file mode 100644 index 0000000000..562fd9191c --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_migrateWebExtensionEngine.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const kExtensionID = "simple@tests.mozilla.org"; + +add_setup(async function () { + useHttpServer("opensearch"); + await AddonTestUtils.promiseStartupManager(); + await SearchTestUtils.useTestEngines("data1"); + await Services.search.init(); +}); + +add_task(async function test_migrateLegacyEngine() { + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: gDataUrl + "simple.xml", + }); + + // Modify the loadpath so it looks like a legacy plugin loadpath + engine.wrappedJSObject._loadPath = `jar:[profile]/extensions/${kExtensionID}.xpi!/simple.xml`; + engine.wrappedJSObject._extensionID = null; + + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + // This should replace the existing engine + let extension = await SearchTestUtils.installSearchExtension( + { + id: "simple", + name: "simple", + search_url: "https://example.com/", + }, + { skipUnload: true } + ); + + engine = Services.search.getEngineByName("simple"); + Assert.equal(engine.wrappedJSObject._loadPath, "[addon]" + kExtensionID); + Assert.equal(engine.wrappedJSObject._extensionID, kExtensionID); + + Assert.equal( + (await Services.search.getDefault()).name, + "simple", + "Should have kept the default engine the same" + ); + + await extension.unload(); +}); + +add_task(async function test_migrateLegacyEngineDifferentName() { + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: gDataUrl + "simple.xml", + }); + + // Modify the loadpath so it looks like an legacy plugin loadpath + engine.wrappedJSObject._loadPath = `jar:[profile]/extensions/${kExtensionID}.xpi!/simple.xml`; + engine.wrappedJSObject._extensionID = null; + + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + // This should replace the existing engine - it has the same id, but a different name. + let extension = await SearchTestUtils.installSearchExtension( + { + id: "simple", + name: "simple search", + search_url: "https://example.com/", + }, + { skipUnload: true } + ); + + engine = Services.search.getEngineByName("simple"); + Assert.equal(engine, null, "Should have removed the old engine"); + + // The engine should have changed its name. + engine = Services.search.getEngineByName("simple search"); + Assert.equal(engine.wrappedJSObject._loadPath, "[addon]" + kExtensionID); + Assert.equal(engine.wrappedJSObject._extensionID, kExtensionID); + + Assert.equal( + (await Services.search.getDefault()).name, + "simple search", + "Should have made the new engine default" + ); + + await extension.unload(); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_missing_engine.js b/toolkit/components/search/tests/xpcshell/test_missing_engine.js new file mode 100644 index 0000000000..3da9fe14a6 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_missing_engine.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test is designed to check the search service keeps working if there's +// a built-in engine missing from the configuration. + +"use strict"; + +const GOOD_CONFIG = [ + { + webExtension: { + id: "engine@search.mozilla.org", + name: "Test search engine", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "channel", + condition: "purpose", + purpose: "contextmenu", + value: "rcs", + }, + { + name: "channel", + condition: "purpose", + purpose: "keyword", + value: "fflb", + }, + ], + suggest_url: + "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}", + }, + appliesTo: [ + { + included: { everywhere: true }, + }, + ], + }, +]; + +const BAD_CONFIG = [ + ...GOOD_CONFIG, + { + webExtension: { + id: "engine-missing@search.mozilla.org", + }, + appliesTo: [ + { + included: { everywhere: true }, + }, + ], + }, +]; + +add_setup(async function () { + SearchTestUtils.useMockIdleService(); + await AddonTestUtils.promiseStartupManager(); + + // This test purposely attempts to load a missing engine. + consoleAllowList.push( + "Could not load engine engine-missing@search.mozilla.org" + ); +}); + +add_task(async function test_startup_with_missing() { + await SearchTestUtils.useTestEngines("data", null, BAD_CONFIG); + + const result = await Services.search.init(); + Assert.ok( + Components.isSuccessCode(result), + "Should have started the search service successfully." + ); + + const engines = await Services.search.getEngines(); + + Assert.deepEqual( + engines.map(e => e.name), + ["Test search engine"], + "Should have listed just the good engine" + ); +}); + +add_task(async function test_update_with_missing() { + let reloadObserved = + SearchTestUtils.promiseSearchNotification("engines-reloaded"); + + await RemoteSettings(SearchUtils.SETTINGS_KEY).emit("sync", { + data: { + current: GOOD_CONFIG, + }, + }); + + SearchTestUtils.idleService._fireObservers("idle"); + + await reloadObserved; + + const engines = await Services.search.getEngines(); + + Assert.deepEqual( + engines.map(e => e.name), + ["Test search engine"], + "Should have just the good engine" + ); + + reloadObserved = + SearchTestUtils.promiseSearchNotification("engines-reloaded"); + + await RemoteSettings(SearchUtils.SETTINGS_KEY).emit("sync", { + data: { + current: BAD_CONFIG, + }, + }); + + SearchTestUtils.idleService._fireObservers("idle"); + + await reloadObserved; + + Assert.deepEqual( + engines.map(e => e.name), + ["Test search engine"], + "Should still have just the good engine" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_nodb_pluschanges.js b/toolkit/components/search/tests/xpcshell/test_nodb_pluschanges.js new file mode 100644 index 0000000000..99b0da3900 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_nodb_pluschanges.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * test_nodb: Start search service without existing settings file. + * + * Ensure that : + * - nothing explodes; + * - if we change the order, search.json.mozlz4 is updated; + * - this search.json.mozlz4 can be parsed; + * - the order stored in search.json.mozlz4 is consistent. + * + * Notes: + * - we install the search engines of test "test_downloadAndAddEngines.js" + * to ensure that this test is independent from locale, commercial agreements + * and configuration of Firefox. + */ + +add_setup(async function () { + useHttpServer(); + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_nodb_pluschanges() { + let engine1 = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engine.xml`, + }); + let engine2 = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engine2.xml`, + }); + await promiseAfterSettings(); + + let search = Services.search; + + await search.moveEngine(engine1, 0); + await search.moveEngine(engine2, 1); + + // This is needed to avoid some reentrency issues in nsSearchService. + info("Next step is forcing flush"); + await new Promise(resolve => executeSoon(resolve)); + + info("Forcing flush"); + let promiseCommit = promiseAfterSettings(); + search.QueryInterface(Ci.nsIObserver).observe(null, "quit-application", ""); + await promiseCommit; + info("Commit complete"); + + // Check that the entries are placed as specified correctly + let metadata = await promiseEngineMetadata(); + Assert.equal(metadata["Test search engine"].order, 1); + Assert.equal(metadata["A second test engine"].order, 2); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_notifications.js b/toolkit/components/search/tests/xpcshell/test_notifications.js new file mode 100644 index 0000000000..cda2888f65 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_notifications.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let engine; +let appDefaultEngine; + +add_setup(async function () { + await AddonTestUtils.promiseStartupManager(); + useHttpServer(); + + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + true + ); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); + + appDefaultEngine = await Services.search.getDefault(); +}); + +add_task(async function test_addingEngine_opensearch() { + const addEngineObserver = new SearchObserver( + [ + // engine-added + // Engine was added to the store by the search service. + SearchUtils.MODIFIED_TYPE.ADDED, + ], + SearchUtils.MODIFIED_TYPE.ADDED + ); + + await SearchTestUtils.promiseNewSearchEngine({ + url: gDataUrl + "engine.xml", + }); + + engine = await addEngineObserver.promise; + + let retrievedEngine = Services.search.getEngineByName("Test search engine"); + Assert.equal(engine, retrievedEngine); +}); + +add_task(async function test_addingEngine_webExtension() { + const addEngineObserver = new SearchObserver( + [ + // engine-added + // Engine was added to the store by the search service. + SearchUtils.MODIFIED_TYPE.ADDED, + ], + SearchUtils.MODIFIED_TYPE.ADDED + ); + + await SearchTestUtils.installSearchExtension({ + name: "Example Engine", + }); + + let webExtensionEngine = await addEngineObserver.promise; + + let retrievedEngine = Services.search.getEngineByName("Example Engine"); + Assert.equal(webExtensionEngine, retrievedEngine); +}); + +async function defaultNotificationTest( + setPrivateDefault, + expectNotificationForPrivate +) { + const defaultObserver = new SearchObserver([ + expectNotificationForPrivate + ? SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE + : SearchUtils.MODIFIED_TYPE.DEFAULT, + ]); + + Services.search[ + setPrivateDefault ? "defaultPrivateEngine" : "defaultEngine" + ] = engine; + await defaultObserver.promise; +} + +add_task(async function test_defaultEngine_notifications() { + await defaultNotificationTest(false, false); +}); + +add_task(async function test_defaultPrivateEngine_notifications() { + await defaultNotificationTest(true, true); +}); + +add_task( + async function test_defaultPrivateEngine_notifications_when_not_enabled() { + await Services.search.setDefault( + appDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + false + ); + + await defaultNotificationTest(true, true); + } +); + +add_task(async function test_removeEngine() { + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.setDefaultPrivate( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + const removedObserver = new SearchObserver([ + SearchUtils.MODIFIED_TYPE.DEFAULT, + SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE, + SearchUtils.MODIFIED_TYPE.REMOVED, + ]); + + await Services.search.removeEngine(engine); + + await removedObserver; +}); diff --git a/toolkit/components/search/tests/xpcshell/test_opensearch.js b/toolkit/components/search/tests/xpcshell/test_opensearch.js new file mode 100644 index 0000000000..bc31e61275 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_opensearch.js @@ -0,0 +1,168 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that OpenSearch engines are installed and set up correctly. + * + * Note: simple.xml, post.xml, suggestion.xml and suggestion-alternate.xml + * all use different namespaces to reflect the possibitities that may be + * installed. + * mozilla-ns.xml uses the mozilla namespace. + */ + +"use strict"; + +const tests = [ + { + file: "simple.xml", + name: "simple", + description: "A small test engine", + searchForm: "https://example.com/", + searchUrl: "https://example.com/search?q=foo", + }, + { + file: "post.xml", + name: "Post", + description: "", + // The POST method is not supported for `rel="searchform"` so we fallback + // to the `SearchForm` url. + searchForm: "http://engine-rel-searchform-post.xml/?search", + searchUrl: "https://example.com/post", + searchPostData: "searchterms=foo", + }, + { + file: "suggestion.xml", + name: "suggestion", + description: "A small engine with suggestions", + queryCharset: "windows-1252", + searchForm: "http://engine-rel-searchform.xml/?search", + searchUrl: "https://example.com/search?q=foo", + suggestUrl: "https://example.com/suggest?suggestion=foo", + }, + { + file: "suggestion-alternate.xml", + name: "suggestion-alternate", + description: "A small engine with suggestions", + searchForm: "https://example.com/", + searchUrl: "https://example.com/search?q=foo", + suggestUrl: "https://example.com/suggest?suggestion=foo", + }, + { + file: "mozilla-ns.xml", + name: "mozilla-ns", + description: "An engine using mozilla namespace", + searchForm: "https://example.com/", + // mozilla-ns.xml also specifies a MozParam. However, they are only + // valid for app-provided engines, and hence the param should not show + // here. + searchUrl: "https://example.com/search?q=foo", + }, + { + file: "searchform-invalid.xml", + name: "searchform-invalid", + description: "Bug 483086 Test 1", + // Should fall back to the root url, if the searchForm url is invalid. + searchForm: "http://mochi.test:8888", + searchUrl: + "http://mochi.test:8888/browser/browser/components/search/test/browser/?search&test=foo", + }, +]; + +add_setup(async function () { + Services.fog.initializeFOG(); + useHttpServer("opensearch"); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +}); + +for (const test of tests) { + add_task(async () => { + info(`Testing ${test.file}`); + let promiseEngineAdded = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.ADDED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: gDataUrl + test.file, + }); + await promiseEngineAdded; + Assert.ok(engine, "Should have installed the engine."); + + Assert.equal(engine.name, test.name, "Should have the correct name"); + Assert.equal( + engine.description, + test.description, + "Should have a description" + ); + + Assert.equal( + engine.wrappedJSObject._loadPath, + `[http]localhost/${test.file}` + ); + + Assert.equal( + engine.queryCharset, + test.queryCharset ?? SearchUtils.DEFAULT_QUERY_CHARSET, + "Should have the expected query charset" + ); + + let submission = engine.getSubmission("foo"); + Assert.equal( + submission.uri.spec, + test.searchUrl, + "Should have the correct search url" + ); + + if (test.searchPostData) { + let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + sis.init(submission.postData); + let data = sis.read(submission.postData.available()); + Assert.equal( + decodeURIComponent(data), + test.searchPostData, + "Should have received the correct POST data" + ); + } else { + Assert.equal( + submission.postData, + null, + "Should have not received any POST data" + ); + } + + Assert.equal( + engine.searchForm, + test.searchForm, + "Should have the correct search form url" + ); + + submission = engine.getSubmission("foo", SearchUtils.URL_TYPE.SUGGEST_JSON); + if (test.suggestUrl) { + Assert.equal( + submission.uri.spec, + test.suggestUrl, + "Should have the correct suggest url" + ); + } else { + Assert.equal(submission, null, "Should not have a suggestion url"); + } + }); +} + +add_task(async function test_telemetry_reporting() { + // Use an engine from the previous tests. + let engine = Services.search.getEngineByName("simple"); + Services.search.defaultEngine = engine; + + await assertGleanDefaultEngine({ + normal: { + engineId: "other-simple", + displayName: "simple", + loadPath: "[http]localhost/simple.xml", + submissionUrl: "blank:", + verified: "verified", + }, + }); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_opensearch_icon.js b/toolkit/components/search/tests/xpcshell/test_opensearch_icon.js new file mode 100644 index 0000000000..68cd9577c1 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_opensearch_icon.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + let server = useHttpServer(); + server.registerContentType("sjs", "sjs"); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +}); + +const ICON_TESTS = [ + { + name: "Big Icon", + image: "bigIcon.ico", + expected: "data:image/png;base64,", + }, + { + name: "Remote Icon", + image: "remoteIcon.ico", + expected: "data:image/x-icon;base64,", + }, + { + name: "SVG Icon", + image: "svgIcon.svg", + expected: "data:image/svg+xml;base64,", + }, +]; + +add_task(async function test_icon_types() { + for (let test of ICON_TESTS) { + info(`Testing ${test.name}`); + + let promiseEngineAdded = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.ADDED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + let promiseEngineChanged = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + const engineData = { + baseURL: gDataUrl, + image: test.image, + name: test.name, + method: "GET", + }; + // The easiest way to test adding the icon is via a generated xml, otherwise + // we have to somehow insert the address of the server into it. + SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engineMaker.sjs?${JSON.stringify(engineData)}`, + }); + let engine = await promiseEngineAdded; + // Ensure this is a nsISearchEngine. + engine.QueryInterface(Ci.nsISearchEngine); + await promiseEngineChanged; + + Assert.ok(engine.getIconURL(), `${test.name} engine has an icon`); + Assert.ok( + engine.getIconURL().startsWith(test.expected), + `${test.name} iconURI starts with the expected information` + ); + } +}); + +add_task(async function test_multiple_icons_in_file() { + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engineImages.xml`, + }); + + Assert.ok(engine.getIconURL().includes("ico16")); + Assert.ok(engine.getIconURL(16).includes("ico16")); + Assert.ok(engine.getIconURL(32).includes("ico32")); + Assert.ok(engine.getIconURL(74).includes("ico74")); + + info("Invalid dimensions should return null until bug 1655070 is fixed."); + Assert.equal(null, engine.getIconURL(50)); +}); + +add_task(async function test_icon_not_in_opensearch_file() { + let engineUrl = gDataUrl + "engine-fr.xml"; + let engine = await Services.search.addOpenSearchEngine( + engineUrl, + "data:image/x-icon;base64,ico16" + ); + + // Even though the icon wasn't specified inside the XML file, it should be + // available both in the iconURI attribute and with getIconURLBySize. + Assert.ok(engine.getIconURL(16).includes("ico16")); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_opensearch_icons_invalid.js b/toolkit/components/search/tests/xpcshell/test_opensearch_icons_invalid.js new file mode 100644 index 0000000000..6db13a0da8 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_opensearch_icons_invalid.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Test that an installed engine can't use a resource URL for an icon */ + +"use strict"; + +add_setup(async function () { + let server = useHttpServer(""); + server.registerContentType("sjs", "sjs"); + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_installedresourceicon() { + // Attempts to load a resource:// url as an icon. + let engine1 = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}opensearch/resourceicon.xml`, + }); + // Attempts to load a chrome:// url as an icon. + let engine2 = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}opensearch/chromeicon.xml`, + }); + + Assert.equal(undefined, engine1.getIconURL()); + Assert.equal(undefined, engine2.getIconURL()); +}); + +add_task(async function test_installedhttpplace() { + let observed = TestUtils.consoleMessageObserved(msg => { + return msg.wrappedJSObject.arguments[0].includes( + "Content type does not match expected" + ); + }); + + // The easiest way to test adding the icon is via a generated xml, otherwise + // we have to somehow insert the address of the server into it. + // Attempts to load a non-image page into an image icon. + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: + `${gDataUrl}data/engineMaker.sjs?` + + JSON.stringify({ + baseURL: gDataUrl, + image: "head_search.js", + name: "invalidicon", + method: "GET", + }), + }); + + await observed; + + Assert.equal( + undefined, + engine.getIconURL(), + "Should not have set an iconURI" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_opensearch_install_errors.js b/toolkit/components/search/tests/xpcshell/test_opensearch_install_errors.js new file mode 100644 index 0000000000..0f1f525c65 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_opensearch_install_errors.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test that various install failures are handled correctly. + */ + +add_setup(async function () { + useHttpServer("opensearch"); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); + + // This test purposely attempts to load an invalid engine. + consoleAllowList.push("_onLoad: Failed to init engine!"); + consoleAllowList.push("Invalid search plugin due to namespace not matching"); +}); + +add_task(async function test_invalid_path_fails() { + await Assert.rejects( + Services.search.addOpenSearchEngine("http://invalid/data/engine.xml", null), + error => { + Assert.equal( + error.result, + Ci.nsISearchService.ERROR_DOWNLOAD_FAILURE, + "Should have returned download failure." + ); + return true; + }, + "Should fail to install an engine with an invalid path." + ); +}); + +add_task(async function test_install_duplicate_fails() { + let engine = await Services.search.addOpenSearchEngine( + gDataUrl + "simple.xml", + null + ); + Assert.equal(engine.name, "simple", "Should have installed the engine."); + + await Assert.rejects( + Services.search.addOpenSearchEngine(gDataUrl + "simple.xml", null), + error => { + Assert.equal( + error.result, + Ci.nsISearchService.ERROR_DUPLICATE_ENGINE, + "Should have returned duplicate failure." + ); + return true; + }, + "Should fail to install a duplicate engine." + ); +}); + +add_task(async function test_invalid_engine_from_dir() { + await Assert.rejects( + Services.search.addOpenSearchEngine(gDataUrl + "invalid.xml", null), + error => { + Assert.equal( + error.result, + Ci.nsISearchService.ERROR_ENGINE_CORRUPTED, + "Should have returned corruption failure." + ); + return true; + }, + "Should fail to install an invalid engine." + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_opensearch_telemetry.js b/toolkit/components/search/tests/xpcshell/test_opensearch_telemetry.js new file mode 100644 index 0000000000..f666e0dd6c --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_opensearch_telemetry.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const { promiseStartupManager, promiseShutdownManager } = AddonTestUtils; + +const openSearchEngineFiles = [ + "secure-and-securely-updated1.xml", + "secure-and-securely-updated2.xml", + "secure-and-securely-updated3.xml", + // An insecure search form should not affect telemetry. + "secure-and-securely-updated-insecure-form.xml", + "secure-and-insecurely-updated1.xml", + "secure-and-insecurely-updated2.xml", + "insecure-and-securely-updated1.xml", + "insecure-and-insecurely-updated1.xml", + "insecure-and-insecurely-updated2.xml", + "secure-and-no-update-url1.xml", + "insecure-and-no-update-url1.xml", + "secure-localhost.xml", + "secure-onionv2.xml", + "secure-onionv3.xml", +]; + +async function verifyTelemetry(probeNameFragment, engineCount, type) { + Services.telemetry.clearScalars(); + await Services.search.runBackgroundChecks(); + + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("parent"), + `browser.searchinit.${probeNameFragment}`, + engineCount, + `Count of ${type} engines: ${engineCount}` + ); +} + +add_setup(async function () { + useHttpServer("opensearch"); + + await promiseStartupManager(); + await Services.search.init(); + + for (let file of openSearchEngineFiles) { + await SearchTestUtils.promiseNewSearchEngine({ url: gDataUrl + file }); + } + + registerCleanupFunction(async () => { + await promiseShutdownManager(); + }); +}); + +add_task(async function () { + verifyTelemetry("secure_opensearch_engine_count", 10, "secure"); + verifyTelemetry("insecure_opensearch_engine_count", 4, "insecure"); + verifyTelemetry("secure_opensearch_update_count", 5, "securely updated"); + verifyTelemetry("insecure_opensearch_update_count", 4, "insecurely updated"); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_opensearch_update.js b/toolkit/components/search/tests/xpcshell/test_opensearch_update.js new file mode 100644 index 0000000000..ecbc1d3826 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_opensearch_update.js @@ -0,0 +1,120 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Test that user-set metadata isn't lost on engine update */ + +"use strict"; + +const KEYWORD = "keyword"; +let timerManager; + +add_setup(async function () { + let server = useHttpServer(""); + server.registerContentType("sjs", "sjs"); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); + + timerManager = Cc["@mozilla.org/updates/timer-manager;1"].getService( + Ci.nsIUpdateTimerManager + ); +}); + +add_task(async function test_installEngine_with_updates_disabled() { + const engineData = { + baseURL: gDataUrl, + name: "test engine", + method: "GET", + updateFile: "opensearch/simple.xml", + }; + + Services.prefs.setBoolPref(SearchUtils.BROWSER_SEARCH_PREF + "update", false); + Assert.ok( + !("search-engine-update-timer" in timerManager.wrappedJSObject._timers), + "Should not have registered the update timer already" + ); + + await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}data/engineMaker.sjs?${JSON.stringify(engineData)}`, + }); + + Assert.ok( + Services.search.getEngineByName("test engine"), + "Should have added the test engine." + ); + Assert.ok( + !("search-engine-update-timer" in timerManager.wrappedJSObject._timers), + "Should have not registered the update timer when updates are disabled" + ); +}); + +add_task(async function test_installEngine_with_updates_enabled() { + const engineData = { + baseURL: gDataUrl, + name: "original engine", + method: "GET", + updateFile: "opensearch/simple.xml", + }; + + Services.prefs.setBoolPref(SearchUtils.BROWSER_SEARCH_PREF + "update", true); + + Assert.ok( + !("search-engine-update-timer" in timerManager.wrappedJSObject._timers), + "Should not have registered the update timer already" + ); + + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}data/engineMaker.sjs?${JSON.stringify(engineData)}`, + }); + + Assert.ok( + "search-engine-update-timer" in timerManager.wrappedJSObject._timers, + "Should have registered the update timer" + ); + + engine.alias = KEYWORD; + await Services.search.moveEngine(engine, 0); + + Assert.ok( + !!Services.search.getEngineByName("original engine"), + "Should be able to get the engine by the original name" + ); + Assert.ok( + !Services.search.getEngineByName("simple"), + "Should not be able to get the engine by the new name" + ); +}); + +add_task(async function test_engineUpdate() { + const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000; + + let promiseUpdate = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + + // set last update to 8 days ago, since the default interval is 7, then + // trigger an update + let engine = Services.search.getEngineByName("original engine"); + engine.wrappedJSObject.setAttr("updateexpir", Date.now() - ONE_DAY_IN_MS * 8); + Services.search.QueryInterface(Ci.nsITimerCallback).notify(null); + + await promiseUpdate; + + Assert.equal(engine.name, "simple", "Should have updated the engine's name"); + + Assert.equal(engine.alias, KEYWORD, "Should have kept the keyword"); + Assert.equal( + engine.wrappedJSObject.getAttr("order"), + 1, + "Should have kept the order" + ); + + Assert.ok( + !!Services.search.getEngineByName("simple"), + "Should be able to get the engine by the new name" + ); + Assert.ok( + !Services.search.getEngineByName("original engine"), + "Should not be able to get the engine by the old name" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_override_allowlist.js b/toolkit/components/search/tests/xpcshell/test_override_allowlist.js new file mode 100644 index 0000000000..f7acaee2f8 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_override_allowlist.js @@ -0,0 +1,399 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests to ensure that when a user installs or uninstalls an add-on, + * we correctly handle the overriding of default and/or parameters + * according to the allowlist. + */ + +"use strict"; + +const kBaseURL = "https://example.com/"; +const kSearchEngineURL = `${kBaseURL}?q={searchTerms}&foo=myparams`; +const kOverriddenEngineName = "Simple Engine"; + +const allowlist = [ + { + thirdPartyId: "test@thirdparty.example.com", + overridesId: "simple@search.mozilla.org", + urls: [], + }, +]; + +const tests = [ + { + title: "test_not_changing_anything", + startupReason: "ADDON_INSTALL", + search_provider: { + is_default: true, + name: "MozParamsTest2", + keyword: "MozSearch", + search_url: kSearchEngineURL, + }, + expected: { + switchToDefaultAllowed: false, + canInstallEngine: true, + overridesEngine: false, + }, + }, + { + title: "test_changing_default_engine", + startupReason: "ADDON_INSTALL", + search_provider: { + is_default: true, + name: kOverriddenEngineName, + keyword: "MozSearch", + search_url: kSearchEngineURL, + }, + expected: { + switchToDefaultAllowed: true, + canInstallEngine: false, + overridesEngine: false, + }, + }, + { + title: "test_changing_default_engine", + startupReason: "ADDON_ENABLE", + search_provider: { + is_default: true, + name: kOverriddenEngineName, + keyword: "MozSearch", + search_url: kSearchEngineURL, + }, + expected: { + switchToDefaultAllowed: true, + canInstallEngine: false, + overridesEngine: false, + }, + }, + { + title: "test_overriding_default_engine", + startupReason: "ADDON_INSTALL", + search_provider: { + is_default: true, + name: kOverriddenEngineName, + keyword: "MozSearch", + search_url: kSearchEngineURL, + }, + allowlistUrls: [ + { + search_url: kSearchEngineURL, + }, + ], + expected: { + switchToDefaultAllowed: true, + canInstallEngine: false, + overridesEngine: true, + searchUrl: kSearchEngineURL, + }, + }, + { + title: "test_overriding_default_engine_enable", + startupReason: "ADDON_ENABLE", + search_provider: { + is_default: true, + name: kOverriddenEngineName, + keyword: "MozSearch", + search_url: kSearchEngineURL, + }, + allowlistUrls: [ + { + search_url: kSearchEngineURL, + }, + ], + expected: { + switchToDefaultAllowed: true, + canInstallEngine: false, + overridesEngine: true, + searchUrl: kSearchEngineURL, + }, + }, + { + title: "test_overriding_default_engine_different_url", + startupReason: "ADDON_INSTALL", + search_provider: { + is_default: true, + name: kOverriddenEngineName, + keyword: "MozSearch", + search_url: kSearchEngineURL + "a", + }, + allowlistUrls: [ + { + search_url: kSearchEngineURL, + }, + ], + expected: { + switchToDefaultAllowed: true, + canInstallEngine: false, + overridesEngine: false, + }, + }, + { + title: "test_overriding_default_engine_get_params", + startupReason: "ADDON_INSTALL", + search_provider: { + is_default: true, + name: kOverriddenEngineName, + keyword: "MozSearch", + search_url: kBaseURL, + search_url_get_params: "q={searchTerms}&enc=UTF-8", + }, + allowlistUrls: [ + { + search_url: kBaseURL, + search_url_get_params: "q={searchTerms}&enc=UTF-8", + }, + ], + expected: { + switchToDefaultAllowed: true, + canInstallEngine: false, + overridesEngine: true, + searchUrl: `${kBaseURL}?q={searchTerms}&enc=UTF-8`, + }, + }, + { + title: "test_overriding_default_engine_different_get_params", + startupReason: "ADDON_INSTALL", + search_provider: { + is_default: true, + name: kOverriddenEngineName, + keyword: "MozSearch", + search_url: kBaseURL, + search_url_get_params: "q={searchTerms}&enc=UTF-8a", + }, + allowlistUrls: [ + { + search_url: kBaseURL, + search_url_get_params: "q={searchTerms}&enc=UTF-8", + }, + ], + expected: { + switchToDefaultAllowed: true, + canInstallEngine: false, + overridesEngine: false, + }, + }, + { + title: "test_overriding_default_engine_post_params", + startupReason: "ADDON_INSTALL", + search_provider: { + is_default: true, + name: kOverriddenEngineName, + keyword: "MozSearch", + search_url: kBaseURL, + search_url_post_params: "q={searchTerms}&enc=UTF-8", + }, + allowlistUrls: [ + { + search_url: kBaseURL, + search_url_post_params: "q={searchTerms}&enc=UTF-8", + }, + ], + expected: { + switchToDefaultAllowed: true, + canInstallEngine: false, + overridesEngine: true, + searchUrl: `${kBaseURL}`, + postData: "q={searchTerms}&enc=UTF-8", + }, + }, + { + title: "test_overriding_default_engine_different_post_params", + startupReason: "ADDON_INSTALL", + search_provider: { + is_default: true, + name: kOverriddenEngineName, + keyword: "MozSearch", + search_url: kBaseURL, + search_url_post_params: "q={searchTerms}&enc=UTF-8a", + }, + allowlistUrls: [ + { + search_url: kBaseURL, + search_url_post_params: "q={searchTerms}&enc=UTF-8", + }, + ], + expected: { + switchToDefaultAllowed: true, + canInstallEngine: false, + overridesEngine: false, + }, + }, + { + title: "test_overriding_default_engine_search_form", + startupReason: "ADDON_INSTALL", + search_provider: { + is_default: true, + name: kOverriddenEngineName, + keyword: "MozSearch", + search_url: kBaseURL, + search_form: "https://example.com/form", + }, + allowlistUrls: [ + { + search_url: kBaseURL, + search_form: "https://example.com/form", + }, + ], + expected: { + switchToDefaultAllowed: true, + canInstallEngine: false, + overridesEngine: true, + searchUrl: `${kBaseURL}`, + searchForm: "https://example.com/form", + }, + }, + { + title: "test_overriding_default_engine_different_search_form", + startupReason: "ADDON_INSTALL", + search_provider: { + is_default: true, + name: kOverriddenEngineName, + keyword: "MozSearch", + search_url: kBaseURL, + search_form: "https://example.com/forma", + }, + allowlistUrls: [ + { + search_url: kBaseURL, + search_form: "https://example.com/form", + }, + ], + expected: { + switchToDefaultAllowed: true, + canInstallEngine: false, + overridesEngine: false, + }, + }, +]; + +let baseExtension; +let remoteSettingsStub; + +add_setup(async function () { + await SearchTestUtils.useTestEngines("simple-engines"); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); + + baseExtension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { + gecko: { + id: "test@thirdparty.example.com", + }, + }, + }, + useAddonManager: "permanent", + }); + await baseExtension.startup(); + + const settings = await RemoteSettings(SearchUtils.SETTINGS_ALLOWLIST_KEY); + remoteSettingsStub = sinon.stub(settings, "get").returns([]); + + registerCleanupFunction(async () => { + await baseExtension.unload(); + }); +}); + +for (const test of tests) { + add_task(async () => { + info(test.title); + + let extension = { + ...baseExtension, + startupReason: test.startupReason, + manifest: { + chrome_settings_overrides: { + search_provider: test.search_provider, + }, + }, + }; + + if (test.expected.overridesEngine) { + remoteSettingsStub.returns([ + { ...allowlist[0], urls: test.allowlistUrls }, + ]); + } + + let result = await Services.search.maybeSetAndOverrideDefault(extension); + Assert.equal( + result.canChangeToAppProvided, + test.expected.switchToDefaultAllowed, + "Should have returned the correct value for allowing switch to default or not." + ); + Assert.equal( + result.canInstallEngine, + test.expected.canInstallEngine, + "Should have returned the correct value for allowing to install the engine or not." + ); + + let engine = await Services.search.getEngineByName(kOverriddenEngineName); + Assert.equal( + !!engine.wrappedJSObject.getAttr("overriddenBy"), + test.expected.overridesEngine, + "Should have correctly overridden or not." + ); + + Assert.equal( + engine.telemetryId, + "simple" + (test.expected.overridesEngine ? "-addon" : ""), + "Should set the correct telemetry Id" + ); + + if (test.expected.overridesEngine) { + let submission = engine.getSubmission("{searchTerms}"); + Assert.equal( + decodeURI(submission.uri.spec), + test.expected.searchUrl, + "Should have set the correct url on an overriden engine" + ); + + if (test.expected.search_form) { + Assert.equal( + engine.wrappedJSObject._searchForm, + test.expected.searchForm, + "Should have overridden the search form." + ); + } + + if (test.expected.postData) { + let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + sis.init(submission.postData); + let data = sis.read(submission.postData.available()); + Assert.equal( + decodeURIComponent(data), + test.expected.postData, + "Should have overridden the postData" + ); + } + + // As we're not testing the WebExtension manager as well, + // set this engine as default so we can check the telemetry data. + let oldDefaultEngine = Services.search.defaultEngine; + Services.search.defaultEngine = engine; + + let engineInfo = Services.search.getDefaultEngineInfo(); + Assert.deepEqual( + engineInfo, + { + defaultSearchEngine: "simple-addon", + defaultSearchEngineData: { + loadPath: SearchUtils.newSearchConfigEnabled + ? "[app]simple@search.mozilla.org" + : "[addon]simple@search.mozilla.org", + name: "Simple Engine", + origin: "default", + submissionURL: test.expected.searchUrl.replace("{searchTerms}", ""), + }, + }, + "Should return the extended identifier and alternate submission url to telemetry" + ); + Services.search.defaultEngine = oldDefaultEngine; + + engine.wrappedJSObject.removeExtensionOverride(); + } + }); +} diff --git a/toolkit/components/search/tests/xpcshell/test_override_allowlist_switch.js b/toolkit/components/search/tests/xpcshell/test_override_allowlist_switch.js new file mode 100644 index 0000000000..cd51e7bdee --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_override_allowlist_switch.js @@ -0,0 +1,721 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests to ensure that we correctly switch and update engines when + * adding and removing application provided engines which overlap + * with engines in the override allow list. + */ + +"use strict"; + +const SEARCH_URL_BASE = "https://example.com/"; +const SEARCH_URL_PARAMS = `?sourceId=enterprise&q={searchTerms}`; +const ENGINE_NAME = "Simple Engine"; + +const ALLOWLIST = [ + { + thirdPartyId: "simpleengine@tests.mozilla.org", + overridesId: "simple@search.mozilla.org", + urls: [ + { search_url: SEARCH_URL_BASE, search_url_get_params: SEARCH_URL_PARAMS }, + ], + }, + { + thirdPartyId: "opensearch@search.mozilla.org", + engineName: ENGINE_NAME, + overridesId: "simple@search.mozilla.org", + urls: [ + { search_url: SEARCH_URL_BASE, search_url_get_params: SEARCH_URL_PARAMS }, + ], + }, +]; + +const CONFIG_SIMPLE_LOCALE_DE = [ + { + webExtension: { + id: "basic@search.mozilla.org", + name: "basic", + search_url: + "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB", + params: [ + { + name: "search", + value: "{searchTerms}", + }, + { + name: "sourceId", + value: "Mozilla-search", + }, + ], + }, + appliesTo: [ + { + included: { everywhere: true }, + default: "yes", + }, + ], + }, + { + webExtension: { + id: "simple@search.mozilla.org", + name: "Simple Engine", + search_url: "https://example.com", + params: [ + { + name: "sourceId", + value: "Mozilla-search", + }, + { + name: "search", + value: "{searchTerms}", + }, + ], + }, + appliesTo: [ + { + included: { locales: { matches: ["de"] } }, + default: "no", + }, + ], + }, +]; + +const CONFIG_SIMPLE_LOCALE_DE_V2 = [ + { + recordType: "engine", + identifier: "basic", + base: { + name: "basic", + urls: { + search: { + base: "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB", + params: [ + { + name: "sourceId", + value: "Mozilla-search", + }, + ], + searchTermParamName: "search", + }, + }, + }, + variants: [ + { + environment: { allRegionsAndLocales: true }, + }, + ], + }, + { + recordType: "engine", + identifier: "simple", + base: { + name: "Simple Engine", + urls: { + search: { + base: "https://example.com", + params: [ + { + name: "sourceId", + value: "Mozilla-search", + }, + ], + searchTermParamName: "search", + }, + }, + }, + variants: [ + { + environment: { locales: ["de"] }, + }, + ], + }, + { + recordType: "defaultEngines", + globalDefault: "basic", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + +const CONFIG_SIMPLE_EVERYWHERE = [ + { + webExtension: { + id: "basic@search.mozilla.org", + name: "basic", + search_url: + "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB", + params: [ + { + name: "search", + value: "{searchTerms}", + }, + { + name: "sourceId", + value: "Mozilla-search", + }, + ], + }, + appliesTo: [ + { + included: { everywhere: true }, + default: "yes", + }, + ], + }, + { + webExtension: { + id: "simple@search.mozilla.org", + name: "Simple Engine", + search_url: "https://example.com", + params: [ + { + name: "sourceId", + value: "Mozilla-search", + }, + { + name: "search", + value: "{searchTerms}", + }, + ], + }, + appliesTo: [ + { + included: { everywhere: true }, + default: "no", + }, + ], + }, +]; + +const CONFIG_SIMPLE_EVERYWHERE_V2 = [ + { + recordType: "engine", + identifier: "basic", + base: { + name: "basic", + urls: { + search: { + base: "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB", + params: [ + { + name: "sourceId", + value: "Mozilla-search", + }, + ], + searchTermParamName: "search", + }, + }, + }, + variants: [ + { + environment: { allRegionsAndLocales: true }, + }, + ], + }, + { + recordType: "engine", + identifier: "simple", + base: { + name: "Simple Engine", + urls: { + search: { + base: "https://example.com", + params: [ + { + name: "sourceId", + value: "Mozilla-search", + }, + ], + searchTermParamName: "search", + }, + }, + }, + variants: [ + { + environment: { allRegionsAndLocales: true }, + }, + ], + }, + { + recordType: "defaultEngines", + globalDefault: "basic", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + +let lastEngineId; +let extension; +let configStub; +let notificationBoxStub; + +add_setup(async function () { + let server = useHttpServer(); + server.registerContentType("sjs", "sjs"); + SearchTestUtils.useMockIdleService(); + configStub = await SearchTestUtils.useTestEngines("simple-engines"); + Services.locale.availableLocales = [ + ...Services.locale.availableLocales, + "en", + "de", + ]; + Services.locale.requestedLocales = ["en"]; + + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); + + const settings = await RemoteSettings(SearchUtils.SETTINGS_ALLOWLIST_KEY); + sinon.stub(settings, "get").returns(ALLOWLIST); + + notificationBoxStub = sinon.stub( + Services.search.wrappedJSObject, + "_showRemovalOfSearchEngineNotificationBox" + ); + + consoleAllowList.push("Failed to load"); +}); + +/** + * Tests that overrides are correctly applied when the deployment of the app + * provided engine is extended into an area, or removed from an area, where a + * user has the WebExtension installed and set as default. + */ +add_task(async function test_app_provided_engine_deployment_extended() { + await assertCorrectlySwitchedWhenExtended(async () => { + info("Change configuration to include engine in user's environment"); + + await SearchTestUtils.updateRemoteSettingsConfig( + SearchUtils.newSearchConfigEnabled + ? CONFIG_SIMPLE_EVERYWHERE_V2 + : CONFIG_SIMPLE_EVERYWHERE + ); + configStub.returns( + SearchUtils.newSearchConfigEnabled + ? CONFIG_SIMPLE_EVERYWHERE_V2 + : CONFIG_SIMPLE_EVERYWHERE + ); + }); + + await assertCorrectlySwitchedWhenRemoved(async () => { + info("Change configuration to remove engine from user's environment"); + + await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_SIMPLE_LOCALE_DE); + configStub.returns(CONFIG_SIMPLE_LOCALE_DE); + }); +}); + +/** + * Tests that overrides are correctly applied when the deployment of the app + * provided engine is extended into an area, or removed from an area, where a + * user has the OpenSearch engine installed and set as default. + */ +add_task( + async function test_app_provided_engine_deployment_extended_opensearch() { + await assertCorrectlySwitchedWhenExtended(async () => { + info("Change configuration to include engine in user's environment"); + + await SearchTestUtils.updateRemoteSettingsConfig( + SearchUtils.newSearchConfigEnabled + ? CONFIG_SIMPLE_EVERYWHERE_V2 + : CONFIG_SIMPLE_EVERYWHERE + ); + configStub.returns( + SearchUtils.newSearchConfigEnabled + ? CONFIG_SIMPLE_EVERYWHERE_V2 + : CONFIG_SIMPLE_EVERYWHERE + ); + }, true); + + await assertCorrectlySwitchedWhenRemoved(async () => { + info("Change configuration to remove engine from user's environment"); + + await SearchTestUtils.updateRemoteSettingsConfig( + SearchUtils.newSearchConfigEnabled + ? CONFIG_SIMPLE_LOCALE_DE_V2 + : CONFIG_SIMPLE_LOCALE_DE + ); + configStub.returns( + SearchUtils.newSearchConfigEnabled + ? CONFIG_SIMPLE_LOCALE_DE_V2 + : CONFIG_SIMPLE_LOCALE_DE + ); + }, true); + } +); + +add_task( + async function test_app_provided_engine_deployment_extended_restart_only() { + await assertCorrectlySwitchedWhenExtended(async () => { + info( + "Change configuration with restart to include engine in user's environment" + ); + + configStub.returns( + SearchUtils.newSearchConfigEnabled + ? CONFIG_SIMPLE_EVERYWHERE_V2 + : CONFIG_SIMPLE_EVERYWHERE + ); + await promiseAfterSettings(); + Services.search.wrappedJSObject.reset(); + await Services.search.init(); + }); + + await assertCorrectlySwitchedWhenRemoved(async () => { + info( + "Change configuration with restart to remove engine from user's environment" + ); + + configStub.returns( + SearchUtils.newSearchConfigEnabled + ? CONFIG_SIMPLE_LOCALE_DE_V2 + : CONFIG_SIMPLE_LOCALE_DE + ); + await promiseAfterSettings(); + Services.search.wrappedJSObject.reset(); + await Services.search.init(); + // Ensure settings have been saved before the engines are added, so that + // we know we won't have race conditions when `addEnginesFromExtension` + // loads the settings itself. + await promiseAfterSettings(); + + // Simulate the add-on manager starting up and telling the + // search service about the add-on again. + let extensionData = { + ...extension.extension, + startupReason: "APP_STARTUP", + }; + await Services.search.addEnginesFromExtension(extensionData); + }); + + let settingsData = await promiseSettingsData(); + Assert.ok( + settingsData.engines.every(e => !e._metaData.overriddenBy), + "Should have cleared the overridden by flag after removal" + ); + } +); + +add_task( + async function test_app_provided_engine_deployment_extended_restart_only_startup_extension() { + await assertCorrectlySwitchedWhenExtended(async () => { + info( + "Change configuration with restart to include engine in user's environment" + ); + + configStub.returns( + SearchUtils.newSearchConfigEnabled + ? CONFIG_SIMPLE_EVERYWHERE_V2 + : CONFIG_SIMPLE_EVERYWHERE + ); + await promiseAfterSettings(); + Services.search.wrappedJSObject.reset(); + await Services.search.init(); + }); + + await assertCorrectlySwitchedWhenRemoved(async () => { + info( + "Change configuration with restart to remove engine from user's environment" + ); + + configStub.returns( + SearchUtils.newSearchConfigEnabled + ? CONFIG_SIMPLE_LOCALE_DE_V2 + : CONFIG_SIMPLE_LOCALE_DE + ); + await promiseAfterSettings(); + Services.search.wrappedJSObject.reset(); + // Simulate the add-on manager starting up and telling the + // search service about the add-on again. + // + // In this test, it does this before init() is called, to + // simulate this being a startup extension. + let extensionData = { + ...extension.extension, + startupReason: "APP_STARTUP", + }; + await Services.search.addEnginesFromExtension(extensionData); + + await Services.search.init(); + }); + + let settingsData = await promiseSettingsData(); + Assert.ok( + settingsData.engines.every(e => !e._metaData.overriddenBy), + "Should have cleared the overridden by flag after removal" + ); + } +); + +add_task( + async function test_app_provided_engine_deployment_extended_opensearch_restart_only() { + await assertCorrectlySwitchedWhenExtended(async () => { + info( + "Change configuration with restart to include engine in user's environment" + ); + + configStub.returns( + SearchUtils.newSearchConfigEnabled + ? CONFIG_SIMPLE_EVERYWHERE_V2 + : CONFIG_SIMPLE_EVERYWHERE + ); + await promiseAfterSettings(); + Services.search.wrappedJSObject.reset(); + await Services.search.init(); + }, true); + + await assertCorrectlySwitchedWhenRemoved(async () => { + info( + "Change configuration with restart to remove engine from user's environment" + ); + + configStub.returns( + SearchUtils.newSearchConfigEnabled + ? CONFIG_SIMPLE_LOCALE_DE_V2 + : CONFIG_SIMPLE_LOCALE_DE + ); + await promiseAfterSettings(); + Services.search.wrappedJSObject.reset(); + await Services.search.init(); + }, true); + + let settingsData = await promiseSettingsData(); + Assert.ok( + settingsData.engines.every(e => !e._metaData.overriddenBy), + "Should have cleared the overridden by flag after removal" + ); + } +); + +/** + * Tests that overrides are correctly applied when the user's environment changes + * e.g. they have the WebExtension installed and change to a locale where the + * application provided engine is (or is not) available. + */ +add_task(async function test_user_environment_changes() { + await assertCorrectlySwitchedWhenExtended(async () => { + info("Change locale to de"); + + await promiseSetLocale("de"); + }); + + await assertCorrectlySwitchedWhenRemoved(async () => { + info("Change locale to en"); + + await promiseSetLocale("en"); + }); +}); + +/** + * Asserts that overrides are handled correctly when a WebExtension is + * installed, and an application provided engine is added for the user. + * + * This is designed to be used prior to assertCorrectlySwitchedWhenRemoved. + * + * @param {Function} changeFn + * A function that applies the change to cause the application provided + * engine to be added for the user. + * @param {boolean} testOpenSearch + * Set to true to test OpenSearch based engines. + */ +async function assertCorrectlySwitchedWhenExtended( + changeFn, + testOpenSearch = false +) { + await SearchTestUtils.updateRemoteSettingsConfig( + SearchUtils.newSearchConfigEnabled + ? CONFIG_SIMPLE_LOCALE_DE_V2 + : CONFIG_SIMPLE_LOCALE_DE + ); + notificationBoxStub.resetHistory(); + + info( + `Install ${ + testOpenSearch ? "OpenSearch" : "WebExtension" + } based engine and set as default` + ); + + let engine; + if (testOpenSearch) { + engine = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engineMaker.sjs?${JSON.stringify({ + baseURL: SEARCH_URL_BASE, + queryString: SEARCH_URL_PARAMS, + name: ENGINE_NAME, + method: "GET", + })}`, + }); + } else { + extension = await SearchTestUtils.installSearchExtension( + { + name: ENGINE_NAME, + search_url: SEARCH_URL_BASE, + search_url_get_params: SEARCH_URL_PARAMS, + }, + { skipUnload: true } + ); + await extension.awaitStartup(); + + engine = Services.search.getEngineById( + "simpleengine@tests.mozilla.orgdefault" + ); + } + + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + // Set a user defined alias. + engine.alias = "star"; + + await assertEngineCorrectlySet({ + expectedId: engine.id, + expectedAlias: "star", + appEngineOverriden: false, + }); + + await changeFn(); + + await assertEngineCorrectlySet({ + expectedId: "simple@search.mozilla.orgdefault", + expectedAlias: "star", + appEngineOverriden: true, + }); + Assert.ok( + notificationBoxStub.notCalled, + "Should not have attempted to display a notification box" + ); + + info("Test restarting search service ensure settings are kept."); + + await promiseAfterSettings(); + Services.search.wrappedJSObject.reset(); + await Services.search.init(); + + if (!testOpenSearch) { + let extensionData = { + ...extension.extension, + startupReason: "APP_STARTUP", + }; + await Services.search.maybeSetAndOverrideDefault(extensionData); + } + + Assert.ok( + notificationBoxStub.notCalled, + "Should not have attempted to display a notification box" + ); + await assertEngineCorrectlySet({ + expectedId: "simple@search.mozilla.orgdefault", + expectedAlias: "star", + appEngineOverriden: true, + }); + + // Save lastEngineId for use in assertCorrectlySwitchedWhenRemoved. + lastEngineId = engine.id; +} + +/** + * Asserts that overrides are handled correctly when a WebExtension is + * installed and overriding an application provided engine, and then the + * application provided engine is removed from the user. + * + * This is designed to be used after to assertCorrectlySwitchedWhenExtended. + * + * @param {Function} changeFn + * A function that applies the change to cause the application provided + * engine to be removed for the user. + * @param {boolean} testOpenSearch + * Set to true to test OpenSearch based engines. + */ +async function assertCorrectlySwitchedWhenRemoved( + changeFn, + testOpenSearch = false +) { + notificationBoxStub.resetHistory(); + + await changeFn(); + + await assertEngineCorrectlySet({ + expectedId: lastEngineId, + expectedAlias: "star", + appEngineOverriden: false, + }); + + info("Test restarting search service to remove application provided engine"); + + await promiseAfterSettings(); + Services.search.wrappedJSObject.reset(); + + if (!testOpenSearch) { + let extensionData = { + ...extension.extension, + startupReason: "APP_STARTUP", + }; + await Services.search.addEnginesFromExtension(extensionData); + } + + await Services.search.init(); + + await assertEngineCorrectlySet({ + expectedId: lastEngineId, + expectedAlias: "star", + appEngineOverriden: false, + }); + + if (testOpenSearch) { + await Services.search.removeEngine( + Services.search.getEngineById(lastEngineId) + ); + } else { + await extension.unload(); + } +} + +async function assertEngineCorrectlySet({ + expectedAlias = "", + expectedId, + appEngineOverriden, +}) { + let engines = await Services.search.getEngines(); + Assert.equal( + engines.filter(e => e.name == ENGINE_NAME).length, + 1, + "Should only be one engine with matching name after changing configuration" + ); + + let defaultEngine = await Services.search.getDefault(); + Assert.equal( + defaultEngine.id, + expectedId, + "Should have kept the third party engine as default" + ); + Assert.equal( + decodeURI(defaultEngine.getSubmission("{searchTerms}").uri.spec), + SEARCH_URL_BASE + SEARCH_URL_PARAMS, + "Should have used the third party engine's URLs" + ); + Assert.equal( + !!defaultEngine.wrappedJSObject.getAttr("overriddenBy"), + appEngineOverriden, + "Should have correctly overridden or not." + ); + + Assert.equal( + defaultEngine.telemetryId, + appEngineOverriden ? "simple-addon" : "other-Simple Engine", + "Should set the correct telemetry Id" + ); + + Assert.equal( + defaultEngine.alias, + expectedAlias, + "Should have the correct alias" + ); +} diff --git a/toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js b/toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js new file mode 100644 index 0000000000..bee90dbb5b --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js @@ -0,0 +1,182 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests getAlternateDomains API. + */ + +"use strict"; + +add_task(async function setup() { + useHttpServer(); + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_parseSubmissionURL() { + let engine1 = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engine.xml`, + }); + let engine2 = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engine-fr.xml`, + }); + + await SearchTestUtils.installSearchExtension({ + name: "bacon_addParam", + keyword: "bacon_addParam", + encoding: "windows-1252", + search_url: "https://www.bacon.test/find", + }); + await SearchTestUtils.installSearchExtension({ + name: "idn_addParam", + keyword: "idn_addParam", + search_url: "https://www.xn--bcher-kva.ch/search", + }); + let engine3 = Services.search.getEngineByName("bacon_addParam"); + let engine4 = Services.search.getEngineByName("idn_addParam"); + + // The following engine provides it's query keyword in + // its template in the form of q={searchTerms} + let engine5 = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engine2.xml`, + }); + + // The following engines cannot identify the search parameter. + await SearchTestUtils.installSearchExtension({ + name: "bacon", + keyword: "bacon", + search_url: "https://www.bacon.moz/search?q=", + search_url_get_params: "", + }); + + await Services.search.setDefault( + engine1, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + // Hide the default engines to prevent them from being used in the search. + for (let engine of await Services.search.getAppProvidedEngines()) { + await Services.search.removeEngine(engine); + } + + // Test the first engine, whose URLs use UTF-8 encoding. + // This also tests the query parameter in a different position not being the + // first parameter. + let url = "https://www.google.com/search?foo=bar&q=caff%C3%A8"; + let result = Services.search.parseSubmissionURL(url); + Assert.equal(result.engine.wrappedJSObject, engine1); + Assert.equal(result.terms, "caff\u00E8"); + + // The second engine uses a locale-specific domain that is an alternate domain + // of the first one, but the second engine should get priority when matching. + // The URL used with this engine uses ISO-8859-1 encoding instead. + url = "https://www.google.fr/search?q=caff%E8"; + result = Services.search.parseSubmissionURL(url); + Assert.equal(result.engine.wrappedJSObject, engine2); + Assert.equal(result.terms, "caff\u00E8"); + + // Test a domain that is an alternate domain of those defined. In this case, + // the first matching engine from the ordered list should be returned. + url = "https://www.google.co.uk/search?q=caff%C3%A8"; + result = Services.search.parseSubmissionURL(url); + Assert.equal(result.engine.wrappedJSObject, engine1); + Assert.equal(result.terms, "caff\u00E8"); + + // We support parsing URLs from a dynamically added engine. + url = "https://www.bacon.test/find?q=caff%E8"; + result = Services.search.parseSubmissionURL(url); + Assert.equal(result.engine, engine3); + Assert.equal(result.terms, "caff\u00E8"); + + // Test URLs with unescaped unicode characters. + url = "https://www.google.com/search?q=foo+b\u00E4r"; + result = Services.search.parseSubmissionURL(url); + Assert.equal(result.engine.wrappedJSObject, engine1); + Assert.equal(result.terms, "foo b\u00E4r"); + + // Test search engines with unescaped IDNs. + url = "https://www.b\u00FCcher.ch/search?q=foo+bar"; + result = Services.search.parseSubmissionURL(url); + Assert.equal(result.engine, engine4); + Assert.equal(result.terms, "foo bar"); + + // Test search engines with escaped IDNs. + url = "https://www.xn--bcher-kva.ch/search?q=foo+bar"; + result = Services.search.parseSubmissionURL(url); + Assert.equal(result.engine, engine4); + Assert.equal(result.terms, "foo bar"); + + // Parsing of parameters from an engine template URL is not supported + // if no matching parameter value template is provided. + Assert.equal( + Services.search.parseSubmissionURL("https://www.bacon.moz/search?q=") + .engine, + null + ); + + // Parsing of parameters from an engine template URL is supported + // if a matching parameter value template is provided. + url = "https://duckduckgo.com/?foo=bar&q=caff%C3%A8"; + result = Services.search.parseSubmissionURL(url); + Assert.equal(result.engine.wrappedJSObject, engine5); + Assert.equal(result.terms, "caff\u00E8"); + + // If the search params are in the template, the query parameter + // doesn't need to be separated from the host by a slash, only by + // by a question mark. + url = "https://duckduckgo.com?foo=bar&q=caff%C3%A8"; + result = Services.search.parseSubmissionURL(url); + Assert.equal(result.engine.wrappedJSObject, engine5); + Assert.equal(result.terms, "caff\u00E8"); + + // HTTP and HTTPS schemes are interchangeable. + url = "https://www.google.com/search?q=caff%C3%A8"; + result = Services.search.parseSubmissionURL(url); + Assert.equal(result.engine.wrappedJSObject, engine1); + Assert.equal(result.terms, "caff\u00E8"); + + // Decoding search terms with multiple spaces should work. + result = Services.search.parseSubmissionURL( + "https://www.google.com/search?q=+with++spaces+" + ); + Assert.equal(result.engine.wrappedJSObject, engine1); + Assert.equal(result.terms, " with spaces "); + + // Parsing search terms with ampersands should work. + result = Services.search.parseSubmissionURL( + "https://www.google.com/search?q=with%26ampersand" + ); + Assert.equal(result.engine.wrappedJSObject, engine1); + Assert.equal(result.terms, "with&ersand"); + + // Capitals in the path should work + result = Services.search.parseSubmissionURL( + "https://www.google.com/SEARCH?q=caps" + ); + Assert.equal(result.engine.wrappedJSObject, engine1); + Assert.equal(result.terms, "caps"); + + // An empty query parameter should work the same. + url = "https://www.google.com/search?q="; + result = Services.search.parseSubmissionURL(url); + Assert.equal(result.engine.wrappedJSObject, engine1); + Assert.equal(result.terms, ""); + + // There should be no match when the path is different. + result = Services.search.parseSubmissionURL( + "https://www.google.com/search/?q=test" + ); + Assert.equal(result.engine, null); + Assert.equal(result.terms, ""); + + // There should be no match when the argument is different. + result = Services.search.parseSubmissionURL( + "https://www.google.com/search?q2=test" + ); + Assert.equal(result.engine, null); + Assert.equal(result.terms, ""); + + // There should be no match for URIs that are not HTTP or HTTPS. + result = Services.search.parseSubmissionURL("file://localhost/search?q=test"); + Assert.equal(result.engine, null); + Assert.equal(result.terms, ""); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_policyEngine.js b/toolkit/components/search/tests/xpcshell/test_policyEngine.js new file mode 100644 index 0000000000..e081e5fb1e --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_policyEngine.js @@ -0,0 +1,185 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that Enterprise Policy Engines can be installed correctly. + */ + +"use strict"; + +const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" +); + +SearchSettings.SETTINGS_INVALIDATION_DELAY = 100; + +/** + * Loads a new enterprise policy, and re-initialise the search service + * with the new policy. Also waits for the search service to write the settings + * file to disk. + * + * @param {object} policy + * The enterprise policy to use. + */ +async function setupPolicyEngineWithJson(policy) { + Services.search.wrappedJSObject.reset(); + + await EnterprisePolicyTesting.setupPolicyEngineWithJson(policy); + + let settingsWritten = SearchTestUtils.promiseSearchNotification( + "write-settings-to-disk-complete" + ); + await Services.search.init(); + await settingsWritten; +} + +add_setup(async function () { + // This initializes the policy engine for xpcshell tests + let policies = Cc["@mozilla.org/enterprisepolicies;1"].getService( + Ci.nsIObserver + ); + policies.observe(null, "policies-startup", null); + + Services.fog.initializeFOG(); + await AddonTestUtils.promiseStartupManager(); + await SearchTestUtils.useTestEngines(); + + SearchUtils.GENERAL_SEARCH_ENGINE_IDS = new Set([ + "engine-resourceicon@search.mozilla.org", + ]); +}); + +add_task(async function test_enterprise_policy_engine() { + await setupPolicyEngineWithJson({ + policies: { + SearchEngines: { + Add: [ + { + Name: "policy", + Description: "Test policy engine", + IconURL: "data:image/gif;base64,R0lGODl", + Alias: "p", + URLTemplate: "https://example.com?q={searchTerms}", + SuggestURLTemplate: "https://example.com/suggest/?q={searchTerms}", + }, + ], + }, + }, + }); + + let engine = Services.search.getEngineByName("policy"); + Assert.ok(engine, "Should have installed the engine."); + + Assert.equal(engine.name, "policy", "Should have the correct name"); + Assert.equal( + engine.description, + "Test policy engine", + "Should have a description" + ); + Assert.deepEqual(engine.aliases, ["p"], "Should have the correct alias"); + + let submission = engine.getSubmission("foo"); + Assert.equal( + submission.uri.spec, + "https://example.com/?q=foo", + "Should have the correct search url" + ); + + submission = engine.getSubmission("foo", SearchUtils.URL_TYPE.SUGGEST_JSON); + Assert.equal( + submission.uri.spec, + "https://example.com/suggest/?q=foo", + "Should have the correct suggest url" + ); + + Services.search.defaultEngine = engine; + + await assertGleanDefaultEngine({ + normal: { + engineId: "other-policy", + displayName: "policy", + loadPath: "[policy]", + submissionUrl: "blank:", + verified: "verified", + }, + }); +}); + +add_task(async function test_enterprise_policy_engine_hidden_persisted() { + // Set the engine alias, and wait for the settings to be written. + let settingsWritten = SearchTestUtils.promiseSearchNotification( + "write-settings-to-disk-complete" + ); + let engine = Services.search.getEngineByName("policy"); + engine.hidden = "p1"; + engine.alias = "p1"; + await settingsWritten; + + // This will reset and re-initialise the search service. + await setupPolicyEngineWithJson({ + policies: { + SearchEngines: { + Add: [ + { + Name: "policy", + Description: "Test policy engine", + IconURL: "data:image/gif;base64,R0lGODl", + Alias: "p", + URLTemplate: "https://example.com?q={searchTerms}", + SuggestURLTemplate: "https://example.com/suggest/?q={searchTerms}", + }, + ], + }, + }, + }); + + engine = Services.search.getEngineByName("policy"); + Assert.equal(engine.alias, "p1", "Should have retained the engine alias"); + Assert.ok(engine.hidden, "Should have kept the engine hidden"); +}); + +add_task(async function test_enterprise_policy_engine_remove() { + // This will reset and re-initialise the search service. + await setupPolicyEngineWithJson({ + policies: {}, + }); + + Assert.ok( + !Services.search.getEngineByName("policy"), + "Should not have the policy engine installed" + ); + + let settings = await promiseSettingsData(); + Assert.ok( + !settings.engines.find(e => e.name == "p1"), + "Should not have the engine settings stored" + ); +}); + +add_task(async function test_enterprise_policy_hidden_default() { + await setupPolicyEngineWithJson({ + policies: { + SearchEngines: { + Remove: ["Test search engine"], + }, + }, + }); + + Services.search.resetToAppDefaultEngine(); + + Assert.equal(Services.search.defaultEngine.name, "engine-resourceicon"); +}); + +add_task(async function test_enterprise_policy_default() { + await setupPolicyEngineWithJson({ + policies: { + SearchEngines: { + Default: "engine-pref", + }, + }, + }); + + Services.search.resetToAppDefaultEngine(); + + Assert.equal(Services.search.defaultEngine.name, "engine-pref"); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_reload_engines.js b/toolkit/components/search/tests/xpcshell/test_reload_engines.js new file mode 100644 index 0000000000..ac63b62e59 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_reload_engines.js @@ -0,0 +1,436 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const CONFIG = [ + { + // Engine initially default, but the defaults will be changed to engine-pref. + webExtension: { + id: "engine@search.mozilla.org", + name: "Test search engine", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "channel", + condition: "purpose", + purpose: "contextmenu", + value: "rcs", + }, + { + name: "channel", + condition: "purpose", + purpose: "keyword", + value: "fflb", + }, + ], + suggest_url: + "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}", + }, + appliesTo: [ + { + included: { everywhere: true }, + default: "yes", + defaultPrivate: "yes", + }, + { + included: { regions: ["FR"] }, + default: "no", + defaultPrivate: "no", + }, + ], + }, + { + // This will become defaults when region is changed to FR. + webExtension: { + id: "engine-pref@search.mozilla.org", + name: "engine-pref", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "code", + condition: "pref", + pref: "code", + }, + { + name: "test", + condition: "pref", + pref: "test", + }, + ], + }, + appliesTo: [ + { + included: { everywhere: true }, + }, + { + included: { regions: ["FR"] }, + default: "yes", + defaultPrivate: "yes", + }, + ], + }, + { + // This engine will get an update when region is changed to FR. + webExtension: { + id: "engine-chromeicon@search.mozilla.org", + name: "engine-chromeicon", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + ], + }, + appliesTo: [ + { + included: { everywhere: true }, + }, + { + included: { regions: ["FR"] }, + extraParams: [ + { name: "c", value: "my-test" }, + { name: "q1", value: "{searchTerms}" }, + ], + }, + ], + }, + { + // This engine will be removed when the region is changed to FR. + webExtension: { + id: "engine-rel-searchform-purpose@search.mozilla.org", + name: "engine-rel-searchform-purpose", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "channel", + condition: "purpose", + purpose: "contextmenu", + value: "rcs", + }, + { + name: "channel", + condition: "purpose", + purpose: "keyword", + value: "fflb", + }, + { + name: "channel", + condition: "purpose", + purpose: "searchbar", + value: "sb", + }, + ], + }, + appliesTo: [ + { + included: { everywhere: true }, + excluded: { regions: ["FR"] }, + }, + ], + }, + { + // This engine will be added when the region is changed to FR. + webExtension: { + id: "engine-reordered@search.mozilla.org", + name: "Test search engine (Reordered)", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "channel", + condition: "purpose", + purpose: "contextmenu", + value: "rcs", + }, + { + name: "channel", + condition: "purpose", + purpose: "keyword", + value: "fflb", + }, + ], + suggest_url: + "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}", + }, + appliesTo: [ + { + included: { regions: ["FR"] }, + }, + ], + }, + { + // This engine will be re-ordered and have a changed name, when moved to FR. + webExtension: { + id: "engine-resourceicon@search.mozilla.org", + name: "engine-resourceicon", + search_url: "https://www.google.com/search", + searchProvider: { + en: { + name: "engine-resourceicon", + search_url: "https://www.google.com/search", + }, + gd: { + name: "engine-resourceicon-gd", + search_url: "https://www.google.com/search", + }, + }, + }, + appliesTo: [ + { + included: { everywhere: true }, + excluded: { regions: ["FR"] }, + }, + { + included: { regions: ["FR"] }, + webExtension: { + locales: ["gd"], + }, + orderHint: 30, + }, + ], + }, + { + // This engine has the same name, but still should be replaced correctly. + webExtension: { + id: "engine-same-name@search.mozilla.org", + name: "engine-same-name", + search_url: "https://www.google.com/search?q={searchTerms}", + searchProvider: { + en: { + name: "engine-same-name", + search_url: "https://www.google.com/search?q={searchTerms}", + }, + gd: { + name: "engine-same-name", + search_url: "https://www.example.com/search?q={searchTerms}", + }, + }, + }, + appliesTo: [ + { + included: { everywhere: true }, + excluded: { regions: ["FR"] }, + }, + { + included: { regions: ["FR"] }, + webExtension: { + locales: ["gd"], + }, + }, + ], + }, +]; + +async function visibleEngines() { + return (await Services.search.getVisibleEngines()).map(e => e.identifier); +} + +add_setup(async function () { + Services.prefs.setBoolPref("browser.search.separatePrivateDefault", true); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + true + ); + + SearchTestUtils.useMockIdleService(); + await SearchTestUtils.useTestEngines("data", null, CONFIG); + await AddonTestUtils.promiseStartupManager(); +}); + +// This is to verify that the loaded configuration matches what we expect for +// the test. +add_task(async function test_initial_config_correct() { + Region._setHomeRegion("", false); + + await Services.search.init(); + + const installedEngines = await Services.search.getAppProvidedEngines(); + Assert.deepEqual( + installedEngines.map(e => e.identifier), + [ + "engine", + "engine-chromeicon", + "engine-pref", + "engine-rel-searchform-purpose", + "engine-resourceicon", + "engine-same-name", + ], + "Should have the correct list of engines installed." + ); + + Assert.equal( + (await Services.search.getDefault()).identifier, + "engine", + "Should have loaded the expected default engine" + ); + + Assert.equal( + (await Services.search.getDefaultPrivate()).identifier, + "engine", + "Should have loaded the expected private default engine" + ); +}); + +add_task(async function test_config_updated_engine_changes() { + // Update the config. + const reloadObserved = + SearchTestUtils.promiseSearchNotification("engines-reloaded"); + const defaultEngineChanged = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.DEFAULT, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + const defaultPrivateEngineChanged = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + + const enginesAdded = []; + const enginesModified = []; + const enginesRemoved = []; + + function enginesObs(subject, topic, data) { + if (data == SearchUtils.MODIFIED_TYPE.ADDED) { + enginesAdded.push(subject.QueryInterface(Ci.nsISearchEngine).identifier); + } else if (data == SearchUtils.MODIFIED_TYPE.CHANGED) { + enginesModified.push( + subject.QueryInterface(Ci.nsISearchEngine).identifier + ); + } else if (data == SearchUtils.MODIFIED_TYPE.REMOVED) { + enginesRemoved.push(subject.QueryInterface(Ci.nsISearchEngine).name); + } + } + Services.obs.addObserver(enginesObs, SearchUtils.TOPIC_ENGINE_MODIFIED); + + Region._setHomeRegion("FR", false); + + await Services.search.wrappedJSObject._maybeReloadEngines(); + + await reloadObserved; + Services.obs.removeObserver(enginesObs, SearchUtils.TOPIC_ENGINE_MODIFIED); + + Assert.deepEqual( + enginesAdded, + ["engine-resourceicon-gd", "engine-reordered"], + "Should have added the correct engines" + ); + + Assert.deepEqual( + enginesModified.sort(), + ["engine", "engine-chromeicon", "engine-pref", "engine-same-name-gd"], + "Should have modified the expected engines" + ); + + Assert.deepEqual( + enginesRemoved, + ["engine-rel-searchform-purpose", "engine-resourceicon"], + "Should have removed the expected engine" + ); + + const installedEngines = await Services.search.getAppProvidedEngines(); + + Assert.deepEqual( + installedEngines.map(e => e.identifier), + [ + "engine-pref", + "engine-resourceicon-gd", + "engine-chromeicon", + "engine-same-name-gd", + "engine", + "engine-reordered", + ], + "Should have the correct list of engines installed in the expected order." + ); + + const newDefault = await defaultEngineChanged; + Assert.equal( + newDefault.QueryInterface(Ci.nsISearchEngine).name, + "engine-pref", + "Should have correctly notified the new default engine" + ); + + const newDefaultPrivate = await defaultPrivateEngineChanged; + Assert.equal( + newDefaultPrivate.QueryInterface(Ci.nsISearchEngine).name, + "engine-pref", + "Should have correctly notified the new default private engine" + ); + + const engineWithParams = await Services.search.getEngineByName( + "engine-chromeicon" + ); + Assert.equal( + engineWithParams.getSubmission("test").uri.spec, + "https://www.google.com/search?c=my-test&q1=test", + "Should have updated the parameters" + ); + + const engineWithSameName = await Services.search.getEngineByName( + "engine-same-name" + ); + Assert.equal( + engineWithSameName.getSubmission("test").uri.spec, + "https://www.example.com/search?q=test", + "Should have correctly switched to the engine of the same name" + ); + + Assert.equal( + Services.search.wrappedJSObject._settings.getMetaDataAttribute( + "useSavedOrder" + ), + false, + "Should not have set the useSavedOrder preference" + ); +}); + +add_task(async function test_user_settings_persist() { + let reload = SearchTestUtils.promiseSearchNotification("engines-reloaded"); + Region._setHomeRegion(""); + await reload; + + Assert.ok( + (await visibleEngines()).includes("engine-rel-searchform-purpose"), + "Rel Searchform engine should be included by default" + ); + + let settingsFileWritten = promiseAfterSettings(); + let engine = await Services.search.getEngineByName( + "engine-rel-searchform-purpose" + ); + await Services.search.removeEngine(engine); + await settingsFileWritten; + + Assert.ok( + !(await visibleEngines()).includes("engine-rel-searchform-purpose"), + "Rel Searchform engine has been removed" + ); + + reload = SearchTestUtils.promiseSearchNotification("engines-reloaded"); + Region._setHomeRegion("FR"); + await reload; + + reload = SearchTestUtils.promiseSearchNotification("engines-reloaded"); + Region._setHomeRegion(""); + await reload; + + Assert.ok( + !(await visibleEngines()).includes("engine-rel-searchform-purpose"), + "Rel Searchform removal should be remembered" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_reload_engines_duplicate.js b/toolkit/components/search/tests/xpcshell/test_reload_engines_duplicate.js new file mode 100644 index 0000000000..3614370a8c --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_reload_engines_duplicate.js @@ -0,0 +1,169 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests reloading engines when a user has an engine installed that is the + * same name as an application provided engine being added to the user's set + * of engines. + * + * Ensures that settings are not automatically taken across. + */ + +"use strict"; + +const CONFIG = [ + { + webExtension: { + id: "engine@search.mozilla.org", + name: "Test search engine", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + ], + }, + appliesTo: [ + { + included: { everywhere: true }, + default: "yes", + }, + ], + }, + { + webExtension: { + id: "engine-pref@search.mozilla.org", + name: "engine-pref", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + ], + }, + appliesTo: [ + { + included: { regions: ["FR"] }, + }, + ], + }, +]; + +const CONFIG_V2 = [ + { + recordType: "engine", + identifier: "engine", + base: { + name: "Test search engine", + urls: { + search: { + base: "https://www.google.com/search", + params: [], + searchTermParamName: "q", + }, + }, + }, + variants: [ + { + environment: { allRegionsAndLocales: true }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-pref", + base: { + name: "engine-pref", + urls: { + search: { + base: "https://www.google.com/search", + params: [], + searchTermParamName: "q", + }, + }, + }, + variants: [ + { + environment: { regions: ["FR"] }, + }, + ], + }, + { + recordType: "defaultEngines", + globalDefault: "engine", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + +add_setup(async () => { + let server = useHttpServer(); + server.registerContentType("sjs", "sjs"); + + // We use a region that doesn't install `engine-pref` by default so that we can + // manually install it first (like when a user installs a browser add-on), and + // then test what happens when we switch regions to one which would install + // `engine-pref`. + Region._setHomeRegion("US", false); + + await SearchTestUtils.useTestEngines( + "data", + null, + SearchUtils.newSearchConfigEnabled ? CONFIG_V2 : CONFIG + ); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +}); + +add_task(async function test_reload_engines_with_duplicate() { + let engines = await Services.search.getEngines(); + + Assert.deepEqual( + engines.map(e => e.name), + ["Test search engine"], + "Should have the expected default engines" + ); + // Simulate a user installing a search engine that shares the same name as an + // application provided search engine not currently installed in their browser. + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engineMaker.sjs?${JSON.stringify({ + baseURL: gDataUrl, + name: "engine-pref", + method: "GET", + })}`, + }); + + engine.alias = "testEngine"; + + let engineId = engine.id; + + Region._setHomeRegion("FR", false); + + await Services.search.wrappedJSObject._maybeReloadEngines(); + + Assert.ok( + !(await Services.search.getEngineById(engineId)), + "Should not have added the duplicate engine" + ); + + engines = await Services.search.getEngines(); + + Assert.deepEqual( + engines.map(e => e.name), + ["Test search engine", "engine-pref"], + "Should have the expected default engines" + ); + + let enginePref = await Services.search.getEngineByName("engine-pref"); + + Assert.equal( + enginePref.alias, + "", + "Should not have copied the alias from the duplicate engine" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_reload_engines_experiment.js b/toolkit/components/search/tests/xpcshell/test_reload_engines_experiment.js new file mode 100644 index 0000000000..ea5cdee3e5 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_reload_engines_experiment.js @@ -0,0 +1,166 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const CONFIG = [ + { + // Just a basic engine that won't be changed. + webExtension: { + id: "engine@search.mozilla.org", + name: "Test search engine", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "channel", + condition: "purpose", + purpose: "contextmenu", + value: "rcs", + }, + { + name: "channel", + condition: "purpose", + purpose: "keyword", + value: "fflb", + }, + ], + suggest_url: + "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}", + }, + appliesTo: [ + { + included: { everywhere: true }, + default: "yes", + }, + ], + }, + { + // This engine will have the locale swapped when the experiment is set. + webExtension: { + id: "engine-same-name@search.mozilla.org", + default_locale: "en", + searchProvider: { + en: { + name: "engine-same-name", + search_url: "https://www.google.com/search?q={searchTerms}", + }, + gd: { + name: "engine-same-name", + search_url: "https://www.example.com/search?q={searchTerms}", + }, + }, + }, + appliesTo: [ + { + included: { everywhere: true }, + webExtension: { + locales: ["en"], + }, + }, + { + included: { everywhere: true }, + webExtension: { + locales: ["gd"], + }, + experiment: "xpcshell", + }, + ], + }, +]; + +add_setup(async function () { + await SearchTestUtils.useTestEngines("data", null, CONFIG); + await AddonTestUtils.promiseStartupManager(); +}); + +// This is to verify that the loaded configuration matches what we expect for +// the test. +add_task(async function test_initial_config_correct() { + await Services.search.init(); + + const installedEngines = await Services.search.getAppProvidedEngines(); + Assert.deepEqual( + installedEngines.map(e => e.identifier), + ["engine", "engine-same-name-en"], + "Should have the correct list of engines installed." + ); + + Assert.equal( + (await Services.search.getDefault()).identifier, + "engine", + "Should have loaded the expected default engine" + ); +}); + +add_task(async function test_config_updated_engine_changes() { + // Update the config. + const reloadObserved = + SearchTestUtils.promiseSearchNotification("engines-reloaded"); + const enginesAdded = []; + const enginesModified = []; + const enginesRemoved = []; + + function enginesObs(subject, topic, data) { + if (data == SearchUtils.MODIFIED_TYPE.ADDED) { + enginesAdded.push(subject.QueryInterface(Ci.nsISearchEngine).identifier); + } else if (data == SearchUtils.MODIFIED_TYPE.CHANGED) { + enginesModified.push( + subject.QueryInterface(Ci.nsISearchEngine).identifier + ); + } else if (data == SearchUtils.MODIFIED_TYPE.REMOVED) { + enginesRemoved.push(subject.QueryInterface(Ci.nsISearchEngine).name); + } + } + Services.obs.addObserver(enginesObs, SearchUtils.TOPIC_ENGINE_MODIFIED); + + Services.prefs.setCharPref( + SearchUtils.BROWSER_SEARCH_PREF + "experiment", + "xpcshell" + ); + + await reloadObserved; + Services.obs.removeObserver(enginesObs, SearchUtils.TOPIC_ENGINE_MODIFIED); + + Assert.deepEqual(enginesAdded, [], "Should have added the correct engines"); + + Assert.deepEqual( + enginesModified.sort(), + ["engine", "engine-same-name-gd"], + "Should have modified the expected engines" + ); + + Assert.deepEqual( + enginesRemoved, + [], + "Should have removed the expected engine" + ); + + const installedEngines = await Services.search.getAppProvidedEngines(); + + Assert.deepEqual( + installedEngines.map(e => e.identifier), + ["engine", "engine-same-name-gd"], + "Should have the correct list of engines installed in the expected order." + ); + + const engineWithSameName = await Services.search.getEngineByName( + "engine-same-name" + ); + Assert.equal( + engineWithSameName.getSubmission("test").uri.spec, + "https://www.example.com/search?q=test", + "Should have correctly switched to the engine of the same name" + ); + + Assert.equal( + Services.search.wrappedJSObject._settings.getMetaDataAttribute( + "useSavedOrder" + ), + false, + "Should not have set the useSavedOrder preference" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_reload_engines_locales.js b/toolkit/components/search/tests/xpcshell/test_reload_engines_locales.js new file mode 100644 index 0000000000..6fa277655d --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_reload_engines_locales.js @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests reloading engines when changing the in-use locale of a WebExtension, + * where the name of the engine changes as well. + */ + +"use strict"; + +const CONFIG = [ + { + webExtension: { + id: "engine@search.mozilla.org", + name: "Test search engine", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "channel", + condition: "purpose", + purpose: "contextmenu", + value: "rcs", + }, + { + name: "channel", + condition: "purpose", + purpose: "keyword", + value: "fflb", + }, + ], + suggest_url: + "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}", + }, + appliesTo: [ + { + included: { everywhere: true }, + default: "yes", + }, + ], + }, + { + webExtension: { + id: "engine-diff-name@search.mozilla.org", + default_locale: "en", + searchProvider: { + en: { + name: "engine-diff-name-en", + search_url: "https://en.wikipedia.com/search", + }, + gd: { + name: "engine-diff-name-gd", + search_url: "https://gd.wikipedia.com/search", + }, + }, + }, + appliesTo: [ + { + included: { everywhere: true }, + excluded: { locales: { matches: ["gd"] } }, + }, + { + included: { locales: { matches: ["gd"] } }, + webExtension: { + locales: ["gd"], + }, + }, + ], + }, +]; + +add_setup(async () => { + Services.locale.availableLocales = [ + ...Services.locale.availableLocales, + "en", + "gd", + ]; + Services.locale.requestedLocales = ["gd"]; + + await SearchTestUtils.useTestEngines("data", null, CONFIG); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +}); + +add_task(async function test_config_updated_engine_changes() { + let engines = await Services.search.getEngines(); + Assert.deepEqual( + engines.map(e => e.name), + ["Test search engine", "engine-diff-name-gd"], + "Should have the correct engines installed" + ); + + let engine = await Services.search.getEngineByName("engine-diff-name-gd"); + Assert.equal( + engine.name, + "engine-diff-name-gd", + "Should have the correct engine name" + ); + Assert.equal( + engine.getSubmission("test").uri.spec, + "https://gd.wikipedia.com/search", + "Should have the gd search url" + ); + + await promiseSetLocale("en"); + + engines = await Services.search.getEngines(); + Assert.deepEqual( + engines.map(e => e.name), + ["Test search engine", "engine-diff-name-en"], + "Should have the correct engines installed after locale change" + ); + + engine = await Services.search.getEngineByName("engine-diff-name-en"); + Assert.equal( + engine.name, + "engine-diff-name-en", + "Should have the correct engine name" + ); + Assert.equal( + engine.getSubmission("test").uri.spec, + "https://en.wikipedia.com/search", + "Should have the en search url" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_remove_engine_notification_box.js b/toolkit/components/search/tests/xpcshell/test_remove_engine_notification_box.js new file mode 100644 index 0000000000..0222334255 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_remove_engine_notification_box.js @@ -0,0 +1,393 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const CONFIG = [ + { + // Engine initially default, but the defaults will be changed to engine-pref. + webExtension: { + id: "engine@search.mozilla.org", + name: "Test search engine", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + ], + }, + appliesTo: [ + { + included: { everywhere: true }, + default: "yes", + }, + { + included: { regions: ["FR"] }, + default: "no", + }, + ], + }, + { + // This will become defaults when region is changed to FR. + webExtension: { + id: "engine-pref@search.mozilla.org", + name: "engine-pref", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + ], + }, + appliesTo: [ + { + included: { everywhere: true }, + }, + { + included: { regions: ["FR"] }, + default: "yes", + }, + ], + }, +]; + +const CONFIG_UPDATED = CONFIG.filter(r => + r.webExtension.id.startsWith("engine-pref") +); + +let stub; +let settingsFilePath; +let userSettings; + +add_setup(async function () { + SearchSettings.SETTINGS_INVALIDATION_DELAY = 100; + SearchTestUtils.useMockIdleService(); + await SearchTestUtils.useTestEngines("data", null, CONFIG); + await AddonTestUtils.promiseStartupManager(); + + stub = sinon.stub( + await Services.search.wrappedJSObject, + "_showRemovalOfSearchEngineNotificationBox" + ); + + settingsFilePath = PathUtils.join(PathUtils.profileDir, SETTINGS_FILENAME); + + Region._setHomeRegion("", false); + + let promiseSaved = promiseAfterSettings(); + await Services.search.init(); + await promiseSaved; + + userSettings = await Services.search.wrappedJSObject._settings.get(); +}); + +// Verify the loaded configuration matches what we expect for the test. +add_task(async function test_initial_config_correct() { + const installedEngines = await Services.search.getAppProvidedEngines(); + Assert.deepEqual( + installedEngines.map(e => e.identifier), + ["engine", "engine-pref"], + "Should have the correct list of engines installed." + ); + + Assert.equal( + (await Services.search.getDefault()).identifier, + "engine", + "Should have loaded the expected default engine" + ); +}); + +add_task(async function test_metadata_undefined() { + let defaultEngineChanged = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.DEFAULT, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + + info("Update region to FR."); + Region._setHomeRegion("FR", false); + + let settings = structuredClone(userSettings); + settings.metaData = undefined; + await reloadEngines(settings); + Assert.ok( + stub.notCalled, + "_reloadEngines should not have shown the notification box." + ); + + settings = structuredClone(userSettings); + settings.metaData = undefined; + await loadEngines(settings); + Assert.ok( + stub.notCalled, + "_loadEngines should not have shown the notification box." + ); + + const newDefault = await defaultEngineChanged; + Assert.equal( + newDefault.QueryInterface(Ci.nsISearchEngine).name, + "engine-pref", + "Should have correctly notified the new default engine." + ); +}); + +add_task(async function test_metadata_changed() { + let metaDataProperties = [ + "locale", + "region", + "channel", + "experiment", + "distroID", + ]; + + for (let name of metaDataProperties) { + let settings = structuredClone(userSettings); + settings.metaData[name] = "test"; + await assert_metadata_changed(settings); + } +}); + +add_task(async function test_default_engine_unchanged() { + let currentEngineName = + Services.search.wrappedJSObject._getEngineDefault(false).name; + + Assert.equal( + currentEngineName, + "Test search engine", + "Default engine should be unchanged." + ); + + await reloadEngines(structuredClone(userSettings)); + Assert.ok( + stub.notCalled, + "_reloadEngines should not have shown the notification box." + ); + + await loadEngines(structuredClone(userSettings)); + Assert.ok( + stub.notCalled, + "_loadEngines should not have shown the notification box." + ); +}); + +add_task(async function test_new_current_engine_is_undefined() { + consoleAllowList.push("No default engine"); + let settings = structuredClone(userSettings); + let getEngineDefaultStub = sinon.stub( + await Services.search.wrappedJSObject, + "_getEngineDefault" + ); + getEngineDefaultStub.returns(undefined); + + await loadEngines(settings); + Assert.ok( + stub.notCalled, + "_loadEngines should not have shown the notification box." + ); + + getEngineDefaultStub.restore(); +}); + +add_task(async function test_current_engine_is_null() { + Services.search.wrappedJSObject._currentEngine = null; + + await reloadEngines(structuredClone(userSettings)); + Assert.ok( + stub.notCalled, + "_reloadEngines should not have shown the notification box." + ); + + let settings = structuredClone(userSettings); + settings.metaData.current = null; + await loadEngines(settings); + Assert.ok( + stub.notCalled, + "_loadEngines should not have shown the notification box." + ); +}); + +add_task(async function test_default_changed_and_metadata_unchanged_exists() { + info("Update region to FR to change engine."); + Region._setHomeRegion("FR", false); + + info("Set user settings metadata to the same properties as cached metadata."); + await Services.search.wrappedJSObject._fetchEngineSelectorEngines(); + userSettings.metaData = { + ...Services.search.wrappedJSObject._settings.getSettingsMetaData(), + appDefaultEngine: "Test search engine", + }; + + await reloadEngines(structuredClone(userSettings)); + Assert.ok( + stub.notCalled, + "_reloadEngines should not show the notification box as the engine still exists." + ); + + // Reset. + Region._setHomeRegion("US", false); + await reloadEngines(structuredClone(userSettings)); +}); + +add_task(async function test_default_engine_changed_and_metadata_unchanged() { + info("Update region to FR to change engine."); + Region._setHomeRegion("FR", false); + + const defaultEngineChanged = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.DEFAULT, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + + info("Set user settings metadata to the same properties as cached metadata."); + await Services.search.wrappedJSObject._fetchEngineSelectorEngines(); + userSettings.metaData = { + ...Services.search.wrappedJSObject._settings.getSettingsMetaData(), + appDefaultEngineId: "engine@search.mozilla.orgdefault", + }; + + // Update config by removing the app default engine + await setConfigToLoad(CONFIG_UPDATED); + + await reloadEngines(structuredClone(userSettings)); + Assert.ok( + stub.calledOnce, + "_reloadEngines should show the notification box." + ); + + Assert.deepEqual( + stub.firstCall.args, + ["Test search engine", "engine-pref"], + "_showRemovalOfSearchEngineNotificationBox should display " + + "'Test search engine' as the engine removed and 'engine-pref' as the new " + + "default engine." + ); + + const newDefault = await defaultEngineChanged; + Assert.equal( + newDefault.QueryInterface(Ci.nsISearchEngine).name, + "engine-pref", + "Should have correctly notified the new default engine" + ); + + info("Reset userSettings.metaData.current engine."); + let settings = structuredClone(userSettings); + settings.metaData.current = Services.search.wrappedJSObject._currentEngine; + + await loadEngines(settings); + Assert.ok(stub.calledTwice, "_loadEngines should show the notification box."); + + Assert.deepEqual( + stub.secondCall.args, + ["Test search engine", "engine-pref"], + "_showRemovalOfSearchEngineNotificationBox should display " + + "'Test search engine' as the engine removed and 'engine-pref' as the new " + + "default engine." + ); +}); + +add_task(async function test_app_default_engine_changed_on_start_up() { + let settings = structuredClone(userSettings); + + // Set the current engine to "" so we can use the app default engine as + // default + settings.metaData.current = ""; + + // Update config by removing the app default engine + await setConfigToLoad(CONFIG_UPDATED); + + await loadEngines(settings); + Assert.ok( + stub.calledThrice, + "_loadEngines should show the notification box." + ); +}); + +add_task(async function test_app_default_engine_change_start_up_still_exists() { + stub.resetHistory(); + let settings = structuredClone(userSettings); + + // Set the current engine to "" so we can use the app default engine as + // default + settings.metaData.current = ""; + settings.metaData.appDefaultEngine = "Test search engine"; + + await setConfigToLoad(CONFIG); + + await loadEngines(settings); + Assert.ok( + stub.notCalled, + "_loadEngines should not show the notification box." + ); +}); + +async function setConfigToLoad(config) { + let searchSettingsObj = await RemoteSettings(SearchUtils.SETTINGS_KEY); + // Restore the get method in order to stub it again in useTestEngines + searchSettingsObj.get.restore(); + Services.search.wrappedJSObject.resetEngineSelector(); + await SearchTestUtils.useTestEngines("data", null, config); +} + +function writeSettings(settings) { + return IOUtils.writeJSON(settingsFilePath, settings, { compress: true }); +} + +async function reloadEngines(settings) { + let promiseSaved = promiseAfterSettings(); + + await Services.search.wrappedJSObject._reloadEngines(settings); + + await promiseSaved; +} + +async function loadEngines(settings) { + await writeSettings(settings); + + let promiseSaved = promiseAfterSettings(); + + Services.search.wrappedJSObject.reset(); + await Services.search.init(); + + await promiseSaved; +} + +async function assert_metadata_changed(settings) { + info("Update region."); + Region._setHomeRegion("FR", false); + await reloadEngines(settings); + Region._setHomeRegion("", false); + + let defaultEngineChanged = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.DEFAULT, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + + await reloadEngines(settings); + Assert.ok( + stub.notCalled, + "_reloadEngines should not have shown the notification box." + ); + + let newDefault = await defaultEngineChanged; + Assert.equal( + newDefault.QueryInterface(Ci.nsISearchEngine).name, + "Test search engine", + "Should have correctly notified the new default engine." + ); + + Region._setHomeRegion("FR", false); + await reloadEngines(settings); + Region._setHomeRegion("", false); + + await loadEngines(settings); + Assert.ok( + stub.notCalled, + "_loadEngines should not have shown the notification box." + ); + + Assert.equal( + Services.search.defaultEngine.name, + "Test search engine", + "Should have correctly notified the new default engine." + ); +} diff --git a/toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js b/toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js new file mode 100644 index 0000000000..6cf73851ce --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test is to ensure that we remove xml files from searchplugins/ in the +// profile directory when a user removes the actual engine from their profile. + +add_setup(async function () { + await SearchTestUtils.useTestEngines("data1"); + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function run_test() { + // Copy an engine to [profile]/searchplugin/ + let dir = do_get_profile().clone(); + dir.append("searchplugins"); + if (!dir.exists()) { + dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + } + do_get_file("data/engine.xml").copyTo(dir, "test-search-engine.xml"); + + let file = dir.clone(); + file.append("test-search-engine.xml"); + Assert.ok(file.exists()); + + let data = await readJSONFile(do_get_file("data/search-legacy.json")); + + // Put the filePath inside the settings file, to simulate what a pre-58 version + // of Firefox would have done. + for (let engine of data.engines) { + if (engine._name == "Test search engine") { + engine.filePath = file.path; + } + } + + await promiseSaveSettingsData(data); + + await Services.search.init(); + + // test the engine is loaded ok. + let engine = Services.search.getEngineByName("Test search engine"); + Assert.notEqual(engine, null, "Should have found the engine"); + + // remove the engine and verify the file has been removed too. + await Services.search.removeEngine(engine); + Assert.ok(!file.exists(), "Should have removed the file."); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_save_sorted_engines.js b/toolkit/components/search/tests/xpcshell/test_save_sorted_engines.js new file mode 100644 index 0000000000..480ee6d11a --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_save_sorted_engines.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Ensure that metadata are stored correctly on disk after: + * - moving an engine + * - removing an engine + * - adding a new engine + * + * Notes: + * - we install the search engines of test "test_downloadAndAddEngines.js" + * to ensure that this test is independent from locale, commercial agreements + * and configuration of Firefox. + */ + +add_setup(async function () { + useHttpServer(); + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_save_sorted_engines() { + let engine1 = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engine.xml`, + }); + let engine2 = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engine2.xml`, + }); + await promiseAfterSettings(); + + let search = Services.search; + + // Test moving the engines + await search.moveEngine(engine1, 0); + await search.moveEngine(engine2, 1); + + // Changes should be commited immediately + await promiseAfterSettings(); + info("Commit complete after moveEngine"); + + // Check that the entries are placed as specified correctly + let metadata = await promiseEngineMetadata(); + Assert.equal(metadata["Test search engine"].order, 1); + Assert.equal(metadata["A second test engine"].order, 2); + + // Test removing an engine + search.removeEngine(engine1); + await promiseAfterSettings(); + info("Commit complete after removeEngine"); + + // Check that the order of the remaining engine was updated correctly + metadata = await promiseEngineMetadata(); + Assert.equal(metadata["A second test engine"].order, 1); + + // Test adding a new engine + await SearchTestUtils.installSearchExtension({ + name: "foo", + keyword: "foo", + }); + + let engine = Services.search.getEngineByName("foo"); + await promiseAfterSettings(); + info("Commit complete after addEngineWithDetails"); + + metadata = await promiseEngineMetadata(); + Assert.ok(engine.aliases.includes("foo")); + Assert.ok(metadata.foo.order > 0); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_searchSuggest.js b/toolkit/components/search/tests/xpcshell/test_searchSuggest.js new file mode 100644 index 0000000000..48769be41e --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_searchSuggest.js @@ -0,0 +1,901 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +/** + * Testing search suggestions from SearchSuggestionController.jsm. + */ + +"use strict"; + +const { FormHistory } = ChromeUtils.importESModule( + "resource://gre/modules/FormHistory.sys.mjs" +); +const { SearchSuggestionController } = ChromeUtils.importESModule( + "resource://gre/modules/SearchSuggestionController.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const ENGINE_NAME = "other"; +const SEARCH_TELEMETRY_LATENCY = "SEARCH_SUGGESTIONS_LATENCY_MS"; + +// We must make sure the FormHistoryStartup component is +// initialized in order for it to respond to FormHistory +// requests from nsFormAutoComplete.js. +var formHistoryStartup = Cc[ + "@mozilla.org/satchel/form-history-startup;1" +].getService(Ci.nsIObserver); +formHistoryStartup.observe(null, "profile-after-change", null); + +var getEngine, postEngine, unresolvableEngine, alternateJSONEngine; + +add_setup(async function () { + Services.prefs.setBoolPref("browser.search.suggest.enabled", true); + // These tests intentionally test broken connections. + consoleAllowList = consoleAllowList.concat([ + "Non-200 status or empty HTTP response: 404", + "Non-200 status or empty HTTP response: 500", + "SearchSuggestionController found an unexpected string value", + "HTTP request timeout", + "HTTP error", + ]); + + let server = useHttpServer(); + server.registerContentType("sjs", "sjs"); + + await AddonTestUtils.promiseStartupManager(); + + let getEngineData = { + baseURL: gDataUrl, + name: "GET suggestion engine", + method: "GET", + }; + + let postEngineData = { + baseURL: gDataUrl, + name: "POST suggestion engine", + method: "POST", + }; + + let unresolvableEngineData = { + baseURL: "http://example.invalid/", + name: "Offline suggestion engine", + method: "GET", + }; + + let alternateJSONSuggestEngineData = { + baseURL: gDataUrl, + name: "Alternative JSON suggestion type", + method: "GET", + alternativeJSONType: true, + }; + + getEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engineMaker.sjs?${JSON.stringify(getEngineData)}`, + }); + postEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engineMaker.sjs?${JSON.stringify(postEngineData)}`, + }); + unresolvableEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engineMaker.sjs?${JSON.stringify(unresolvableEngineData)}`, + }); + alternateJSONEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engineMaker.sjs?${JSON.stringify( + alternateJSONSuggestEngineData + )}`, + }); + + registerCleanupFunction(async () => { + // Remove added form history entries + await updateSearchHistory("remove", null); + Services.prefs.clearUserPref("browser.search.suggest.enabled"); + }); +}); + +// Begin tests + +add_task(async function simple_no_result_promise() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + + let controller = new SearchSuggestionController(); + let result = await controller.fetch("no remote", false, getEngine); + Assert.equal(result.term, "no remote"); + Assert.equal(result.local.length, 0); + Assert.equal(result.remote.length, 0); + + assertLatencyHistogram(histogram, true); +}); + +add_task(async function simple_remote_no_local_result() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + + let controller = new SearchSuggestionController(); + let result = await controller.fetch("mo", false, getEngine); + Assert.equal(result.term, "mo"); + Assert.equal(result.local.length, 0); + Assert.equal(result.remote.length, 3); + Assert.equal(result.remote[0].value, "Mozilla"); + Assert.equal(result.remote[1].value, "modern"); + Assert.equal(result.remote[2].value, "mom"); + + assertLatencyHistogram(histogram, true); +}); + +add_task(async function simple_remote_no_local_result_telemetry() { + Services.telemetry.clearScalars(); + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + + let controller = new SearchSuggestionController(); + await controller.fetch("mo", false, getEngine); + + let scalars = {}; + const key = "browser.search.data_transferred"; + + await TestUtils.waitForCondition(() => { + scalars = + Services.telemetry.getSnapshotForKeyedScalars("main", false).parent || {}; + return key in scalars; + }, "should have the expected keyed scalars"); + + const scalar = scalars[key]; + Assert.ok(`sggt-${ENGINE_NAME}` in scalar, "correct telemetry category"); + Assert.notEqual(scalar[`sggt-${ENGINE_NAME}`], 0, "bandwidth logged"); + + assertLatencyHistogram(histogram, true); +}); + +add_task(async function simple_remote_no_local_result_alternative_type() { + let controller = new SearchSuggestionController(); + let result = await controller.fetch("mo", false, alternateJSONEngine); + Assert.equal(result.term, "mo"); + Assert.equal(result.local.length, 0); + Assert.equal(result.remote.length, 3); + Assert.equal(result.remote[0].value, "Mozilla"); + Assert.equal(result.remote[1].value, "modern"); + Assert.equal(result.remote[2].value, "mom"); +}); + +add_task(async function remote_term_case_mismatch() { + let controller = new SearchSuggestionController(); + let result = await controller.fetch("Query Case Mismatch", false, getEngine); + Assert.equal(result.term, "Query Case Mismatch"); + Assert.equal(result.remote.length, 1); + Assert.equal(result.remote[0].value, "Query Case Mismatch"); +}); + +add_task(async function simple_local_no_remote_result() { + await updateSearchHistory("bump", "no remote entries"); + + let controller = new SearchSuggestionController(); + let result = await controller.fetch("no remote", false, getEngine); + Assert.equal(result.term, "no remote"); + Assert.equal(result.local.length, 1); + Assert.equal(result.local[0].value, "no remote entries"); + Assert.equal(result.remote.length, 0); + + await updateSearchHistory("remove", "no remote entries"); +}); + +add_task(async function simple_non_ascii() { + await updateSearchHistory("bump", "I ❤️ XUL"); + + let controller = new SearchSuggestionController(); + let result = await controller.fetch("I ❤️", false, getEngine); + Assert.equal(result.term, "I ❤️"); + Assert.equal(result.local.length, 1); + Assert.equal(result.local[0].value, "I ❤️ XUL"); + Assert.equal(result.remote.length, 1); + Assert.equal(result.remote[0].value, "I ❤️ Mozilla"); +}); + +add_task(async function both_local_remote_result_dedupe() { + await updateSearchHistory("bump", "Mozilla"); + + let controller = new SearchSuggestionController(); + let result = await controller.fetch("mo", false, getEngine); + Assert.equal(result.term, "mo"); + Assert.equal(result.local.length, 1); + Assert.equal(result.local[0].value, "Mozilla"); + Assert.equal(result.remote.length, 2); + Assert.equal(result.remote[0].value, "modern"); + Assert.equal(result.remote[1].value, "mom"); +}); + +add_task(async function POST_both_local_remote_result_dedupe() { + let controller = new SearchSuggestionController(); + let result = await controller.fetch("mo", false, postEngine); + Assert.equal(result.term, "mo"); + Assert.equal(result.local.length, 1); + Assert.equal(result.local[0].value, "Mozilla"); + Assert.equal(result.remote.length, 2); + Assert.equal(result.remote[0].value, "modern"); + Assert.equal(result.remote[1].value, "mom"); +}); + +add_task(async function both_local_remote_result_dedupe2() { + await updateSearchHistory("bump", "mom"); + + let controller = new SearchSuggestionController(); + let result = await controller.fetch("mo", false, getEngine); + Assert.equal(result.term, "mo"); + Assert.equal(result.local.length, 2); + Assert.equal(result.local[0].value, "mom"); + Assert.equal(result.local[1].value, "Mozilla"); + Assert.equal(result.remote.length, 1); + Assert.equal(result.remote[0].value, "modern"); +}); + +add_task(async function both_local_remote_result_dedupe3() { + // All of the server entries also exist locally + await updateSearchHistory("bump", "modern"); + + let controller = new SearchSuggestionController(); + let result = await controller.fetch("mo", false, getEngine); + Assert.equal(result.term, "mo"); + Assert.equal(result.local.length, 3); + Assert.equal(result.local[0].value, "modern"); + Assert.equal(result.local[1].value, "mom"); + Assert.equal(result.local[2].value, "Mozilla"); + Assert.equal(result.remote.length, 0); +}); + +add_task(async function valid_tail_results() { + let controller = new SearchSuggestionController(); + let result = await controller.fetch("tail query", false, getEngine); + Assert.equal(result.term, "tail query"); + Assert.equal(result.local.length, 0); + Assert.equal(result.remote.length, 3); + Assert.equal(result.remote[0].value, "tail query normal"); + Assert.ok(!result.remote[0].matchPrefix); + Assert.ok(!result.remote[0].tail); + Assert.equal(result.remote[1].value, "tail query tail 1"); + Assert.equal(result.remote[1].matchPrefix, "… "); + Assert.equal(result.remote[1].tail, "tail 1"); + Assert.equal(result.remote[2].value, "tail query tail 2"); + Assert.equal(result.remote[2].matchPrefix, "… "); + Assert.equal(result.remote[2].tail, "tail 2"); +}); + +add_task(async function alt_tail_results() { + let controller = new SearchSuggestionController(); + let result = await controller.fetch("tailalt query", false, getEngine); + Assert.equal(result.term, "tailalt query"); + Assert.equal(result.local.length, 0); + Assert.equal(result.remote.length, 3); + Assert.equal(result.remote[0].value, "tailalt query normal"); + Assert.ok(!result.remote[0].matchPrefix); + Assert.ok(!result.remote[0].tail); + Assert.equal(result.remote[1].value, "tailalt query tail 1"); + Assert.equal(result.remote[1].matchPrefix, "… "); + Assert.equal(result.remote[1].tail, "tail 1"); + Assert.equal(result.remote[2].value, "tailalt query tail 2"); + Assert.equal(result.remote[2].matchPrefix, "… "); + Assert.equal(result.remote[2].tail, "tail 2"); +}); + +add_task(async function invalid_tail_results() { + let controller = new SearchSuggestionController(); + let result = await controller.fetch("tailjunk query", false, getEngine); + Assert.equal(result.term, "tailjunk query"); + Assert.equal(result.local.length, 0); + Assert.equal(result.remote.length, 3); + Assert.equal(result.remote[0].value, "tailjunk query normal"); + Assert.ok(!result.remote[0].matchPrefix); + Assert.ok(!result.remote[0].tail); + Assert.equal(result.remote[1].value, "tailjunk query tail 1"); + Assert.ok(!result.remote[1].matchPrefix); + Assert.ok(!result.remote[1].tail); + Assert.equal(result.remote[2].value, "tailjunk query tail 2"); + Assert.ok(!result.remote[2].matchPrefix); + Assert.ok(!result.remote[2].tail); +}); + +add_task(async function too_few_tail_results() { + let controller = new SearchSuggestionController(); + let result = await controller.fetch("tailjunk few query", false, getEngine); + Assert.equal(result.term, "tailjunk few query"); + Assert.equal(result.local.length, 0); + Assert.equal(result.remote.length, 3); + Assert.equal(result.remote[0].value, "tailjunk few query normal"); + Assert.ok(!result.remote[0].matchPrefix); + Assert.ok(!result.remote[0].tail); + Assert.equal(result.remote[1].value, "tailjunk few query tail 1"); + Assert.ok(!result.remote[1].matchPrefix); + Assert.ok(!result.remote[1].tail); + Assert.equal(result.remote[2].value, "tailjunk few query tail 2"); + Assert.ok(!result.remote[2].matchPrefix); + Assert.ok(!result.remote[2].tail); +}); + +add_task(async function empty_rich_results() { + let controller = new SearchSuggestionController(); + let result = await controller.fetch("richempty query", false, getEngine); + Assert.equal(result.term, "richempty query"); + Assert.equal(result.local.length, 0); + Assert.equal(result.remote.length, 3); + Assert.equal(result.remote[0].value, "richempty query normal"); + Assert.ok(!result.remote[0].matchPrefix); + Assert.ok(!result.remote[0].tail); + Assert.equal(result.remote[1].value, "richempty query tail 1"); + Assert.ok(!result.remote[1].matchPrefix); + Assert.ok(!result.remote[1].tail); + Assert.equal(result.remote[2].value, "richempty query tail 2"); + Assert.ok(!result.remote[2].matchPrefix); + Assert.ok(!result.remote[2].tail); +}); + +add_task(async function tail_offset_index() { + let controller = new SearchSuggestionController(); + let result = await controller.fetch("tail tail 1 t", false, getEngine); + Assert.equal(result.term, "tail tail 1 t"); + Assert.equal(result.local.length, 0); + Assert.equal(result.remote.length, 3); + Assert.equal(result.remote[1].value, "tail tail 1 t tail 1"); + Assert.equal(result.remote[1].matchPrefix, "… "); + Assert.equal(result.remote[1].tail, "tail 1"); + Assert.equal(result.remote[1].tailOffsetIndex, 14); +}); + +add_task(async function fetch_twice_in_a_row() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + + // Two entries since the first will match the first fetch but not the second. + await updateSearchHistory("bump", "delay local"); + await updateSearchHistory("bump", "delayed local"); + + let controller = new SearchSuggestionController(); + let resultPromise1 = controller.fetch("delay", false, getEngine); + + // A second fetch while the server is still waiting to return results leads to an abort. + let resultPromise2 = controller.fetch("delayed ", false, getEngine); + await resultPromise1.then(results => Assert.equal(null, results)); + + let result = await resultPromise2; + Assert.equal(result.term, "delayed "); + Assert.equal(result.local.length, 1); + Assert.equal(result.local[0].value, "delayed local"); + Assert.equal(result.remote.length, 1); + Assert.equal(result.remote[0].value, "delayed "); + + // Only the second fetch's latency should be recorded since the first fetch + // was aborted and latencies for aborted fetches are not recorded. + assertLatencyHistogram(histogram, true); +}); + +add_task(async function both_identical_with_more_than_max_results() { + // Add letters A through Z to form history which will match the server + for ( + let charCode = "A".charCodeAt(); + charCode <= "Z".charCodeAt(); + charCode++ + ) { + await updateSearchHistory( + "bump", + "letter " + String.fromCharCode(charCode) + ); + } + + let controller = new SearchSuggestionController(); + controller.maxLocalResults = 7; + controller.maxRemoteResults = 10; + let result = await controller.fetch("letter ", false, getEngine); + Assert.equal(result.term, "letter "); + Assert.equal(result.local.length, 7); + for (let i = 0; i < controller.maxLocalResults; i++) { + Assert.equal( + result.local[i].value, + "letter " + String.fromCharCode("A".charCodeAt() + i) + ); + } + Assert.equal(result.local.length + result.remote.length, 10); + for (let i = 0; i < result.remote.length; i++) { + Assert.equal( + result.remote[i].value, + "letter " + + String.fromCharCode("A".charCodeAt() + controller.maxLocalResults + i) + ); + } +}); + +add_task(async function noremote_maxLocal() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + + let controller = new SearchSuggestionController(); + controller.maxLocalResults = 2; // (should be ignored because no remote results) + controller.maxRemoteResults = 0; + let result = await controller.fetch("letter ", false, getEngine); + Assert.equal(result.term, "letter "); + Assert.equal(result.local.length, 26); + for (let i = 0; i < result.local.length; i++) { + Assert.equal( + result.local[i].value, + "letter " + String.fromCharCode("A".charCodeAt() + i) + ); + } + Assert.equal(result.remote.length, 0); + + assertLatencyHistogram(histogram, false); +}); + +add_task(async function someremote_maxLocal() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + + let controller = new SearchSuggestionController(); + controller.maxLocalResults = 2; + controller.maxRemoteResults = 4; + let result = await controller.fetch("letter ", false, getEngine); + Assert.equal(result.term, "letter "); + Assert.equal(result.local.length, 2); + for (let i = 0; i < result.local.length; i++) { + Assert.equal( + result.local[i].value, + "letter " + String.fromCharCode("A".charCodeAt() + i) + ); + } + Assert.equal(result.remote.length, 2); + // "A" and "B" will have been de-duped, start at C for remote results + for (let i = 0; i < result.remote.length; i++) { + Assert.equal( + result.remote[i].value, + "letter " + String.fromCharCode("C".charCodeAt() + i) + ); + } + + assertLatencyHistogram(histogram, true); +}); + +add_task(async function one_of_each() { + let controller = new SearchSuggestionController(); + controller.maxLocalResults = 1; + controller.maxRemoteResults = 2; + let result = await controller.fetch("letter ", false, getEngine); + Assert.equal(result.term, "letter "); + Assert.equal(result.local.length, 1); + Assert.equal(result.local[0].value, "letter A"); + Assert.equal(result.remote.length, 1); + Assert.equal(result.remote[0].value, "letter B"); +}); + +add_task(async function local_result_returned_remote_result_disabled() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + let controller = new SearchSuggestionController(); + controller.maxLocalResults = 1; + controller.maxRemoteResults = 1; + let result = await controller.fetch("letter ", false, getEngine); + Assert.equal(result.term, "letter "); + Assert.equal(result.local.length, 26); + for (let i = 0; i < 26; i++) { + Assert.equal( + result.local[i].value, + "letter " + String.fromCharCode("A".charCodeAt() + i) + ); + } + Assert.equal(result.remote.length, 0); + assertLatencyHistogram(histogram, false); + Services.prefs.setBoolPref("browser.search.suggest.enabled", true); +}); + +add_task( + async function local_result_returned_remote_result_disabled_after_creation_of_controller() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + let controller = new SearchSuggestionController(); + controller.maxLocalResults = 1; + controller.maxRemoteResults = 1; + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + let result = await controller.fetch("letter ", false, getEngine); + Assert.equal(result.term, "letter "); + Assert.equal(result.local.length, 26); + for (let i = 0; i < 26; i++) { + Assert.equal( + result.local[i].value, + "letter " + String.fromCharCode("A".charCodeAt() + i) + ); + } + Assert.equal(result.remote.length, 0); + assertLatencyHistogram(histogram, false); + Services.prefs.setBoolPref("browser.search.suggest.enabled", true); + } +); + +add_task( + async function one_of_each_disabled_before_creation_enabled_after_creation_of_controller() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + let controller = new SearchSuggestionController(); + controller.maxLocalResults = 1; + controller.maxRemoteResults = 2; + Services.prefs.setBoolPref("browser.search.suggest.enabled", true); + let result = await controller.fetch("letter ", false, getEngine); + Assert.equal(result.term, "letter "); + Assert.equal(result.local.length, 1); + Assert.equal(result.local[0].value, "letter A"); + Assert.equal(result.remote.length, 1); + Assert.equal(result.remote[0].value, "letter B"); + + assertLatencyHistogram(histogram, true); + + Services.prefs.setBoolPref("browser.search.suggest.enabled", true); + } +); + +add_task(async function one_local_zero_remote() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + let controller = new SearchSuggestionController(); + controller.maxLocalResults = 1; + controller.maxRemoteResults = 0; + let result = await controller.fetch("letter ", false, getEngine); + Assert.equal(result.term, "letter "); + Assert.equal(result.local.length, 26); + for (let i = 0; i < 26; i++) { + Assert.equal( + result.local[i].value, + "letter " + String.fromCharCode("A".charCodeAt() + i) + ); + } + Assert.equal(result.remote.length, 0); + assertLatencyHistogram(histogram, false); +}); + +add_task(async function zero_local_one_remote() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + let controller = new SearchSuggestionController(); + controller.maxLocalResults = 0; + controller.maxRemoteResults = 1; + let result = await controller.fetch("letter ", false, getEngine); + Assert.equal(result.term, "letter "); + Assert.equal(result.local.length, 0); + Assert.equal(result.remote.length, 1); + Assert.equal(result.remote[0].value, "letter A"); + assertLatencyHistogram(histogram, true); +}); + +add_task(async function stop_search() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + let controller = new SearchSuggestionController(result => { + do_throw("The callback shouldn't be called after stop()"); + }); + let resultPromise = controller.fetch("mo", false, getEngine); + controller.stop(); + await resultPromise.then(result => { + Assert.equal(null, result); + }); + assertLatencyHistogram(histogram, false); +}); + +add_task(async function empty_searchTerm() { + // Empty searches don't go to the server but still get form history. + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + let controller = new SearchSuggestionController(); + let result = await controller.fetch("", false, getEngine); + Assert.equal(result.term, ""); + Assert.ok(!!result.local.length); + Assert.equal(result.remote.length, 0); + assertLatencyHistogram(histogram, false); +}); + +add_task(async function slow_timeout() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + + // Make the server return suggestions on a delay longer than the timeout of + // the suggestion controller. + let delayMs = 3 * SearchSuggestionController.REMOTE_TIMEOUT_DEFAULT; + let searchString = `delay${delayMs} `; + + // Add a local result. + let localValue = searchString + " local result"; + await updateSearchHistory("bump", localValue); + + // Do a search. The remote fetch should time out but the local result should + // be returned. + let controller = new SearchSuggestionController(); + let result = await controller.fetch(searchString, false, getEngine); + Assert.equal(result.term, searchString); + Assert.equal(result.local.length, 1); + Assert.equal(result.local[0].value, localValue); + Assert.equal(result.remote.length, 0); + + // The remote fetch isn't done yet, so the latency histogram should not be + // updated. + assertLatencyHistogram(histogram, false); + + // Wait for the remote fetch to finish. + await new Promise(r => setTimeout(r, delayMs)); + + // Now the latency histogram should be updated. + assertLatencyHistogram(histogram, true); +}); + +add_task(async function slow_timeout_2() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + + // Make the server return suggestions on a delay longer the timeout of the + // suggestion controller. + let delayMs = 3 * SearchSuggestionController.REMOTE_TIMEOUT_DEFAULT; + let searchString = `delay${delayMs} `; + + // Add a local result. + let localValue = searchString + " local result"; + await updateSearchHistory("bump", localValue); + + // Do two searches using the same controller. Both times, the remote fetches + // should time out and only the local result should be returned. The second + // search should abort the remote fetch of the first search, and the remote + // fetch of the second search should be ongoing when the second search + // finishes. + let controller = new SearchSuggestionController(); + for (let i = 0; i < 2; i++) { + let result = await controller.fetch(searchString, false, getEngine); + Assert.equal(result.term, searchString); + Assert.equal(result.local.length, 1); + Assert.equal(result.local[0].value, localValue); + Assert.equal(result.remote.length, 0); + } + + // The remote fetch of the second search isn't done yet, so the latency + // histogram should not be updated. + assertLatencyHistogram(histogram, false); + + // Wait for the second remote fetch to finish. + await new Promise(r => setTimeout(r, delayMs)); + + // Now the latency histogram should be updated, and only the remote fetch of + // the second search should be recorded. + assertLatencyHistogram(histogram, true); +}); + +add_task(async function slow_stop() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + + // Make the server return suggestions on a delay longer the timeout of the + // suggestion controller. + let delayMs = 3 * SearchSuggestionController.REMOTE_TIMEOUT_DEFAULT; + let searchString = `delay${delayMs} `; + + // Do a search but stop it before it finishes. Wait a tick before stopping it + // to better simulate the real world. + let controller = new SearchSuggestionController(); + let resultPromise = controller.fetch(searchString, false, getEngine); + await TestUtils.waitForTick(); + controller.stop(); + let result = await resultPromise; + Assert.equal(result, null, "No result should be returned"); + + // The remote fetch should have been aborted by stopping the controller, but + // wait for the timeout period just to make sure it's done. + await new Promise(r => setTimeout(r, delayMs)); + + // Since the latencies of aborted fetches are not recorded, the latency + // histogram should not be updated. + assertLatencyHistogram(histogram, false); +}); + +// Error handling + +add_task(async function remote_term_mismatch() { + await updateSearchHistory("bump", "Query Mismatch Entry"); + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + + let controller = new SearchSuggestionController(); + let result = await controller.fetch("Query Mismatch", false, getEngine); + Assert.equal(result.term, "Query Mismatch"); + Assert.equal(result.local.length, 1); + Assert.equal(result.local[0].value, "Query Mismatch Entry"); + Assert.equal(result.remote.length, 0); + + assertLatencyHistogram(histogram, true); +}); + +add_task(async function http_404() { + await updateSearchHistory("bump", "HTTP 404 Entry"); + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + + let controller = new SearchSuggestionController(); + let result = await controller.fetch("HTTP 404", false, getEngine); + Assert.equal(result.term, "HTTP 404"); + Assert.equal(result.local.length, 1); + Assert.equal(result.local[0].value, "HTTP 404 Entry"); + Assert.equal(result.remote.length, 0); + + assertLatencyHistogram(histogram, true); +}); + +add_task(async function http_500() { + await updateSearchHistory("bump", "HTTP 500 Entry"); + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + + let controller = new SearchSuggestionController(); + let result = await controller.fetch("HTTP 500", false, getEngine); + Assert.equal(result.term, "HTTP 500"); + Assert.equal(result.local.length, 1); + Assert.equal(result.local[0].value, "HTTP 500 Entry"); + Assert.equal(result.remote.length, 0); + + assertLatencyHistogram(histogram, true); +}); + +add_task(async function invalid_response_does_not_throw() { + let controller = new SearchSuggestionController(); + // Although the server will return invalid json, the error is handled by + // the suggestion controller, and so we receive no results. + let result = await controller.fetch("invalidJSON", false, getEngine); + Assert.equal(result.term, "invalidJSON"); + Assert.equal(result.local.length, 0); + Assert.equal(result.remote.length, 0); +}); + +add_task(async function invalid_content_type_treated_as_json() { + let controller = new SearchSuggestionController(); + // An invalid content type is overridden as we expect all the responses to + // be JSON. + let result = await controller.fetch("invalidContentType", false, getEngine); + Assert.equal(result.term, "invalidContentType"); + Assert.equal(result.local.length, 0); + Assert.equal(result.remote.length, 1); + Assert.equal(result.remote[0].value, "invalidContentType response"); +}); + +add_task(async function unresolvable_server() { + await updateSearchHistory("bump", "Unresolvable Server Entry"); + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram( + SEARCH_TELEMETRY_LATENCY + ); + + let controller = new SearchSuggestionController(); + let result = await controller.fetch( + "Unresolvable Server", + false, + unresolvableEngine + ); + Assert.equal(result.term, "Unresolvable Server"); + Assert.equal(result.local.length, 1); + Assert.equal(result.local[0].value, "Unresolvable Server Entry"); + Assert.equal(result.remote.length, 0); + + assertLatencyHistogram(histogram, true); +}); + +// Exception handling + +add_task(async function missing_pb() { + Assert.throws(() => { + let controller = new SearchSuggestionController(); + controller.fetch("No privacy"); + }, /priva/i); +}); + +add_task(async function missing_engine() { + Assert.throws(() => { + let controller = new SearchSuggestionController(); + controller.fetch("No engine", false); + }, /engine/i); +}); + +add_task(async function invalid_engine() { + Assert.throws(() => { + let controller = new SearchSuggestionController(); + controller.fetch("invalid engine", false, {}); + }, /engine/i); +}); + +add_task(async function no_results_requested() { + Assert.throws(() => { + let controller = new SearchSuggestionController(); + controller.maxLocalResults = 0; + controller.maxRemoteResults = 0; + controller.fetch("No results requested", false, getEngine); + }, /result/i); +}); + +add_task(async function minus_one_results_requested() { + Assert.throws(() => { + let controller = new SearchSuggestionController(); + controller.maxLocalResults = -1; + controller.fetch("-1 results requested", false, getEngine); + }, /result/i); +}); + +add_task(async function test_userContextId() { + let controller = new SearchSuggestionController(); + controller._fetchRemote = function ( + searchTerm, + engine, + privateMode, + userContextId + ) { + Assert.equal(userContextId, 1); + return Promise.withResolvers(); + }; + + controller.fetch("test", false, getEngine, 1); +}); + +// Non-English characters + +add_task(async function suggestions_contain_escaped_unicode() { + let controller = new SearchSuggestionController(); + let result = await controller.fetch("stü", false, getEngine); + Assert.equal(result.term, "stü"); + Assert.equal(result.local.length, 0); + Assert.equal(result.remote.length, 2); + Assert.equal(result.remote[0].value, "stühle"); + Assert.equal(result.remote[1].value, "stüssy"); +}); + +// Helpers + +function updateSearchHistory(operation, value) { + return FormHistory.update({ + op: operation, + fieldname: "searchbar-history", + value, + }); +} + +function assertLatencyHistogram(histogram, shouldRecord) { + let snapshot = histogram.snapshot(); + info("Checking latency snapshot: " + JSON.stringify(snapshot)); + + // Build a map from engine ID => number of non-zero values recorded for it. + let valueCountByEngineId = Object.entries(snapshot).reduce( + (memo, [key, data]) => { + memo[key] = Object.values(data.values).filter(v => v != 0); + return memo; + }, + {} + ); + + let expected = shouldRecord ? { [ENGINE_NAME]: [1] } : {}; + Assert.deepEqual( + valueCountByEngineId, + expected, + shouldRecord ? "Latency histogram updated" : "Latency histogram not updated" + ); +} diff --git a/toolkit/components/search/tests/xpcshell/test_searchSuggest_cookies.js b/toolkit/components/search/tests/xpcshell/test_searchSuggest_cookies.js new file mode 100644 index 0000000000..cfa9e56144 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_searchSuggest_cookies.js @@ -0,0 +1,143 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that search suggestions from SearchSuggestionController.jsm don't store + * cookies. + */ + +"use strict"; + +const { SearchSuggestionController } = ChromeUtils.importESModule( + "resource://gre/modules/SearchSuggestionController.sys.mjs" +); + +// We must make sure the FormHistoryStartup component is +// initialized in order for it to respond to FormHistory +// requests from nsFormAutoComplete.js. +var formHistoryStartup = Cc[ + "@mozilla.org/satchel/form-history-startup;1" +].getService(Ci.nsIObserver); +formHistoryStartup.observe(null, "profile-after-change", null); + +function countCacheEntries() { + info("Enumerating cache entries"); + return new Promise(resolve => { + let storage = Services.cache2.diskCacheStorage( + Services.loadContextInfo.default + ); + storage.asyncVisitStorage( + { + onCacheStorageInfo(num, consumption) { + this._num = num; + }, + onCacheEntryInfo(uri) { + info("Found cache entry: " + uri.asciiSpec); + }, + onCacheEntryVisitCompleted() { + resolve(this._num || 0); + }, + }, + true /* Do walk entries */ + ); + }); +} + +function countCookieEntries() { + info("Enumerating cookies"); + let cookies = Services.cookies.cookies; + let cookieCount = 0; + for (let cookie of cookies) { + info( + "Cookie:" + cookie.rawHost + " " + JSON.stringify(cookie.originAttributes) + ); + cookieCount++; + break; + } + return cookieCount; +} + +let engines; + +add_setup(async function () { + await AddonTestUtils.promiseStartupManager(); + + Services.prefs.setBoolPref("browser.search.suggest.enabled", true); + Services.prefs.setBoolPref("browser.search.suggest.enabled.private", true); + + registerCleanupFunction(async () => { + // Clean up all the data. + await new Promise(resolve => + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve) + ); + Services.prefs.clearUserPref("browser.search.suggest.enabled"); + Services.prefs.clearUserPref("browser.search.suggest.enabled.private"); + }); + + let server = useHttpServer(); + server.registerContentType("sjs", "sjs"); + + let unicodeName = ["\u30a8", "\u30c9"].join(""); + engines = [ + await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engineMaker.sjs?${JSON.stringify({ + baseURL: gDataUrl, + name: unicodeName, + method: "GET", + })}`, + }), + await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engineMaker.sjs?${JSON.stringify({ + baseURL: gDataUrl, + name: "engine two", + method: "GET", + })}`, + }), + ]; + + // Clean up all the data. + await new Promise(resolve => + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve) + ); + Assert.equal(await countCacheEntries(), 0, "The cache should be empty"); + Assert.equal(await countCookieEntries(), 0, "Should not find any cookie"); +}); + +add_task(async function test_private_mode() { + await test_engine(true); +}); +add_task(async function test_normal_mode() { + await test_engine(false); +}); + +async function test_engine(privateMode) { + info(`Testing ${privateMode ? "private" : "normal"} mode`); + let controller = new SearchSuggestionController(); + let result = await controller.fetch("no results", privateMode, engines[0]); + Assert.equal(result.local.length, 0, "Should have no local suggestions"); + Assert.equal(result.remote.length, 0, "Should have no remote suggestions"); + + result = await controller.fetch("cookie", privateMode, engines[1]); + Assert.equal(result.local.length, 0, "Should have no local suggestions"); + Assert.equal(result.remote.length, 0, "Should have no remote suggestions"); + Assert.equal(await countCacheEntries(), 0, "The cache should be empty"); + Assert.equal(await countCookieEntries(), 0, "Should not find any cookie"); + + let firstPartyDomain1 = controller.firstPartyDomains.get(engines[0].name); + Assert.ok( + /^[\.a-z0-9-]+\.search\.suggestions\.mozilla/.test(firstPartyDomain1), + "Check firstPartyDomain1" + ); + + let firstPartyDomain2 = controller.firstPartyDomains.get(engines[1].name); + Assert.ok( + /^[\.a-z0-9-]+\.search\.suggestions\.mozilla/.test(firstPartyDomain2), + "Check firstPartyDomain2" + ); + + Assert.notEqual( + firstPartyDomain1, + firstPartyDomain2, + "Check firstPartyDomain id unique per engine" + ); +} diff --git a/toolkit/components/search/tests/xpcshell/test_searchSuggest_extraParams.js b/toolkit/components/search/tests/xpcshell/test_searchSuggest_extraParams.js new file mode 100644 index 0000000000..8ecf4b02f3 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_searchSuggest_extraParams.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_CONFIG = [ + { + webExtension: { + id: "get@search.mozilla.org", + name: "Get Engine", + search_url: "https://example.com", + search_url_get_params: "webExtension=1&search={searchTerms}", + suggest_url: "https://example.com", + suggest_url_get_params: "webExtension=1&suggest={searchTerms}", + }, + appliesTo: [{ included: { everywhere: true } }], + suggestExtraParams: [ + { + name: "custom_param", + pref: "test_pref_param", + condition: "pref", + }, + ], + }, +]; + +add_setup(async function () { + await SearchTestUtils.useTestEngines("method-extensions", null, TEST_CONFIG); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +}); + +add_task(async function test_custom_suggest_param() { + let engine = Services.search.getEngineByName("Get Engine"); + Assert.notEqual(engine, null, "Should have found an engine"); + + let submissionSuggest = engine.getSubmission( + "bar", + SearchUtils.URL_TYPE.SUGGEST_JSON + ); + Assert.equal( + submissionSuggest.uri.spec, + "https://example.com/?webExtension=1&suggest=bar", + "Suggest URLs should match" + ); + + let defaultBranch = Services.prefs.getDefaultBranch("browser.search."); + defaultBranch.setCharPref("param.test_pref_param", "good"); + + let nextSubmissionSuggest = engine.getSubmission( + "bar", + SearchUtils.URL_TYPE.SUGGEST_JSON + ); + Assert.equal( + nextSubmissionSuggest.uri.spec, + "https://example.com/?custom_param=good&webExtension=1&suggest=bar", + "Suggest URLs should include custom param" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_searchSuggest_private.js b/toolkit/components/search/tests/xpcshell/test_searchSuggest_private.js new file mode 100644 index 0000000000..063f3ada49 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_searchSuggest_private.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that search suggestions from SearchSuggestionController.jsm operate + * correctly in private mode. + */ + +"use strict"; + +const { SearchSuggestionController } = ChromeUtils.importESModule( + "resource://gre/modules/SearchSuggestionController.sys.mjs" +); + +let engine; + +add_setup(async function () { + Services.prefs.setBoolPref("browser.search.suggest.enabled", true); + + let server = useHttpServer(); + server.registerContentType("sjs", "sjs"); + + await AddonTestUtils.promiseStartupManager(); + + const engineData = { + baseURL: gDataUrl, + name: "GET suggestion engine", + method: "GET", + }; + + engine = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engineMaker.sjs?${JSON.stringify(engineData)}`, + }); +}); + +add_task(async function test_suggestions_in_private_mode_enabled() { + Services.prefs.setBoolPref("browser.search.suggest.enabled.private", true); + + let controller = new SearchSuggestionController(); + controller.maxLocalResults = 0; + controller.maxRemoteResults = 1; + let result = await controller.fetch("mo", true, engine); + Assert.equal(result.remote.length, 1); +}); + +add_task(async function test_suggestions_in_private_mode_disabled() { + Services.prefs.setBoolPref("browser.search.suggest.enabled.private", false); + + let controller = new SearchSuggestionController(); + controller.maxLocalResults = 0; + controller.maxRemoteResults = 1; + let result = await controller.fetch("mo", true, engine); + Assert.equal(result.remote.length, 0); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_searchTermFromResult.js b/toolkit/components/search/tests/xpcshell/test_searchTermFromResult.js new file mode 100644 index 0000000000..700c6a3450 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_searchTermFromResult.js @@ -0,0 +1,350 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Tests searchTermFromResult API. + */ + +let defaultEngine; + +// The test string contains special characters to ensure +// that they are encoded/decoded properly. +const TERM = "c;,?:@&=+$-_.!~*'()# d\u00E8f"; +const TERM_ENCODED = "c%3B%2C%3F%3A%40%26%3D%2B%24-_.!~*'()%23+d%C3%A8f"; + +add_setup(async function () { + await SearchTestUtils.useTestEngines("data", null, [ + { + webExtension: { + id: "engine-purposes@search.mozilla.org", + name: "Test Engine With Purposes", + search_url: "https://www.example.com/search", + params: [ + { + name: "form", + condition: "purpose", + purpose: "keyword", + value: "MOZKEYWORD", + }, + { + name: "form", + condition: "purpose", + purpose: "contextmenu", + value: "MOZCONTEXT", + }, + { + name: "form", + condition: "purpose", + purpose: "newtab", + value: "MOZNEWTAB", + }, + { + name: "form", + condition: "purpose", + purpose: "searchbar", + value: "MOZSEARCHBAR", + }, + { + name: "form", + condition: "purpose", + purpose: "homepage", + value: "MOZHOMEPAGE", + }, + { + name: "pc", + value: "FIREFOX", + }, + { + name: "channel", + condition: "pref", + pref: "testChannelEnabled", + }, + { + name: "q", + value: "{searchTerms}", + }, + ], + }, + appliesTo: [ + { + included: { everywhere: true }, + default: "yes", + }, + ], + }, + ]); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); + + defaultEngine = Services.search.getEngineByName("Test Engine With Purposes"); +}); + +add_task(async function test_searchTermFromResult_withAllPurposes() { + for (let purpose of Object.values(SearchUtils.PARAM_PURPOSES)) { + let uri = defaultEngine.getSubmission(TERM, null, purpose).uri; + let searchTerm = defaultEngine.searchTermFromResult(uri); + Assert.equal( + searchTerm, + TERM, + `Should return the correct url for purpose: ${purpose}` + ); + } +}); + +add_task(async function test_searchTermFromResult() { + // Internationalized Domain Name search engine. + await SearchTestUtils.installSearchExtension({ + name: "idn_addParam", + keyword: "idn_addParam", + search_url: "https://www.xn--bcher-kva.ch/search", + }); + let engineEscapedIDN = Services.search.getEngineByName("idn_addParam"); + + // Setup server for french engine. + await useHttpServer(); + + // For ISO-8859-1 encoding testing. + let engineISOCharset = await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engine-fr.xml`, + }); + + // For Windows-1252 encoding testing. + await SearchTestUtils.installSearchExtension({ + name: "bacon_addParam", + keyword: "bacon_addParam", + encoding: "windows-1252", + search_url: "https://www.bacon.test/find", + }); + let engineWinCharset = Services.search.getEngineByName("bacon_addParam"); + + // Verify getValidEngineUrl returns a URL that can return a search term. + let testUrl = getValidEngineUrl(); + Assert.equal( + getTerm(testUrl), + TERM, + "Should get term from a url generated by getSubmission." + ); + + testUrl = getValidEngineUrl(); + testUrl.pathname = "/SEARCH"; + Assert.equal( + getTerm(testUrl), + TERM, + "Should get term even if path is not the same case as the engine." + ); + + let url = `https://www.xn--bcher-kva.ch/search?q=${TERM_ENCODED}`; + Assert.equal( + getTerm(url, engineEscapedIDN), + TERM, + "Should get term from IDNs urls." + ); + + url = `http://www.google.fr/search?q=caf%E8+au+lait&ie=iso-8859-1&oe=iso-8859-1`; + Assert.equal( + getTerm(url, engineISOCharset), + "caf\u00E8 au lait", + "Should get term from ISO-8859-1 encoded url containing a search term." + ); + + url = `http://www.google.fr/search?&ie=iso-8859-1&oe=iso-8859-1&q=`; + Assert.equal( + getTerm(url, engineISOCharset), + "", + "Should get a blank string from ISO-8859-1 encoded url missing a search term" + ); + + url = "https://www.bacon.test/find?q=caf%E8+au+lait"; + Assert.equal( + getTerm(url, engineWinCharset), + "caf\u00E8 au lait", + "Should get term from Windows-1252 encoded url containing a search term." + ); + + url = "https://www.bacon.test/find?q="; + Assert.equal( + getTerm(url, engineWinCharset), + "", + "Should get a blank string from Windows-1252 encoded url missing a search term." + ); + + url = "about:blank"; + Assert.equal(getTerm(url), "", "Should get a blank string from about:blank."); + + url = "about:newtab"; + Assert.equal( + getTerm(url), + "", + "Should get a blank string from about:newtab." + ); +}); + +// Use a version of the url that should return a term and make minute +// modifications that should cause it to return a blank value. +add_task(async function test_searchTermFromResult_blank() { + let url = getValidEngineUrl(); + url.searchParams.set("hello", "world"); + Assert.equal( + getTerm(url), + "", + "Should get a blank string from url containing query param name not recognized by the engine." + ); + + url = getValidEngineUrl(); + url.protocol = "http"; + Assert.equal( + getTerm(url), + "", + "Should get a blank string from url that has a different scheme from the engine." + ); + + url = getValidEngineUrl(); + url.protocol = "http"; + Assert.equal( + getTerm(url), + "", + "Should get a blank string from url that has a different path from the engine." + ); + + url = getValidEngineUrl(); + url.host = "images.example.com"; + Assert.equal( + getTerm(url), + "", + "Should get a blank string from url that has a different host from the engine." + ); + + url = getValidEngineUrl(); + url.host = "example.com"; + Assert.equal( + getTerm(url), + "", + "Should get a blank string from url that has a different host from the engine." + ); + + url = getValidEngineUrl(); + url.searchParams.set("form", "MOZUNKNOWN"); + Assert.equal( + getTerm(url), + "", + "Should get a blank string from a url that has an un-recognized form value." + ); + + url = getValidEngineUrl(); + url.searchParams.set("q", ""); + Assert.equal( + getTerm(url), + "", + "Should get a blank string from a url with a missing search query value." + ); + + url = getValidEngineUrl(); + url.searchParams.delete("q"); + Assert.equal( + getTerm(url), + "", + "Should get a blank string from a url with a missing search query name." + ); + + url = getValidEngineUrl(); + url.searchParams.delete("pc"); + Assert.equal( + getTerm(url), + "", + "Should get a blank string from a url with a missing a query parameter." + ); + + url = getValidEngineUrl(); + url.searchParams.delete("form"); + Assert.equal( + getTerm(url), + "", + "Should get a blank string from a url with a missing a query parameter." + ); +}); + +add_task(async function test_searchTermFromResult_prefParam() { + const defaultBranch = Services.prefs.getDefaultBranch( + SearchUtils.BROWSER_SEARCH_PREF + ); + + defaultBranch.setCharPref("param.testChannelEnabled", "yes"); + + let url = getValidEngineUrl(true); + Assert.equal(getTerm(url), TERM, "Should get term after pref is turned on."); + + url.searchParams.delete("channel"); + Assert.equal( + getTerm(url), + "", + "Should get a blank string if pref is on and channel param is missing." + ); + + defaultBranch.setCharPref("param.testChannelEnabled", ""); + url = getValidEngineUrl(true); + Assert.equal(getTerm(url), TERM, "Should get term after pref is turned off."); + + url.searchParams.set("channel", "yes"); + Assert.equal( + getTerm(url), + "", + "Should get a blank string if pref is turned off but channel param is present." + ); +}); + +// searchTermFromResult attempts to look into the template of a search +// engine if query params aren't present in the url.params, so make sure +// it works properly and fails gracefully. +add_task(async function test_searchTermFromResult_paramsInSearchUrl() { + await SearchTestUtils.installSearchExtension({ + name: "engine_params_in_search_url", + search_url: "https://example.com/?q={searchTerms}&pc=firefox", + search_url_get_params: "", + }); + let testEngine = Services.search.getEngineByName( + "engine_params_in_search_url" + ); + let url = `https://example.com/?q=${TERM_ENCODED}&pc=firefox`; + Assert.equal( + getTerm(url, testEngine), + TERM, + "Should get term from an engine with params in its search url." + ); + + url = `https://example.com/?q=${TERM_ENCODED}`; + Assert.equal( + getTerm(url, testEngine), + "", + "Should get a blank string when not all params are present." + ); + + await SearchTestUtils.installSearchExtension({ + name: "engine_params_in_search_url_without_delimiter", + search_url: "https://example.com/q={searchTerms}", + search_url_get_params: "", + }); + testEngine = Services.search.getEngineByName( + "engine_params_in_search_url_without_delimiter" + ); + url = `https://example.com/?q=${TERM_ENCODED}&pc=firefox&page=1`; + Assert.equal( + getTerm(url, testEngine), + "", + "Should get a blank string from an engine with no params and no delimiter in its url." + ); +}); + +function getTerm(url, searchEngine = defaultEngine) { + return searchEngine.searchTermFromResult(Services.io.newURI(url.toString())); +} + +// Return a new instance of a submission URL so that it can modified +// and tested again. Allow callers to force the cache to update, especially +// if the engine is expected to have updated. +function getValidEngineUrl(updateCache = false) { + if (updateCache || !this._submissionUrl) { + this._submissionUrl = defaultEngine.getSubmission(TERM, null).uri.spec; + } + return new URL(this._submissionUrl); +} diff --git a/toolkit/components/search/tests/xpcshell/test_searchUrlDomain.js b/toolkit/components/search/tests/xpcshell/test_searchUrlDomain.js new file mode 100644 index 0000000000..ea7db5e516 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_searchUrlDomain.js @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests searchUrlDomain API. + */ + +"use strict"; + +add_setup(async function () { + await SearchTestUtils.useTestEngines("data", null); + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_resultDomain() { + await Services.search.init(); + + let engine = Services.search.getEngineByName("Test search engine"); + + Assert.equal(engine.searchUrlDomain, "www.google.com"); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_selectedEngine.js b/toolkit/components/search/tests/xpcshell/test_selectedEngine.js new file mode 100644 index 0000000000..f42670156c --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_selectedEngine.js @@ -0,0 +1,155 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const kDefaultEngineName = "engine1"; + +add_setup(async function () { + useHttpServer(); + await AddonTestUtils.promiseStartupManager(); + await SearchTestUtils.useTestEngines("data1"); + Assert.ok(!Services.search.isInitialized); + Services.prefs.setBoolPref( + "browser.search.removeEngineInfobar.enabled", + false + ); +}); + +// Check that the default engine matches the defaultenginename pref +add_task(async function test_defaultEngine() { + await Services.search.init(); + await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engine.xml`, + }); + + Assert.equal(Services.search.defaultEngine.name, kDefaultEngineName); +}); + +// Setting the search engine should be persisted across restarts. +add_task(async function test_persistAcrossRestarts() { + // Set the engine through the API. + await Services.search.setDefault( + Services.search.getEngineByName(kTestEngineName), + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + Assert.equal(Services.search.defaultEngine.name, kTestEngineName); + await promiseAfterSettings(); + + // Check that the a hash was saved. + let metadata = await promiseGlobalMetadata(); + Assert.equal(metadata.defaultEngineIdHash.length, 44); + + // Re-init and check the engine is still the same. + Services.search.wrappedJSObject.reset(); + await Services.search.init(true); + Assert.equal(Services.search.defaultEngine.name, kTestEngineName); + + // Cleanup (set the engine back to default). + Services.search.resetToAppDefaultEngine(); + Assert.equal(Services.search.defaultEngine.name, kDefaultEngineName); +}); + +// An engine set without a valid hash should be ignored. +add_task(async function test_ignoreInvalidHash() { + // Set the engine through the API. + await Services.search.setDefault( + Services.search.getEngineByName(kTestEngineName), + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + Assert.equal(Services.search.defaultEngine.name, kTestEngineName); + await promiseAfterSettings(); + + // Then mess with the file (make the hash invalid). + let metadata = await promiseGlobalMetadata(); + metadata.defaultEngineIdHash = "invalid"; + await promiseSaveGlobalMetadata(metadata); + + // Re-init the search service, and check that the json file is ignored. + Services.search.wrappedJSObject.reset(); + await Services.search.init(true); + Assert.equal(Services.search.defaultEngine.name, kDefaultEngineName); +}); + +// Resetting the engine to the default should remove the saved value. +add_task(async function test_settingToDefault() { + // Set the engine through the API. + await Services.search.setDefault( + Services.search.getEngineByName(kTestEngineName), + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + Assert.equal(Services.search.defaultEngine.name, kTestEngineName); + await promiseAfterSettings(); + + // Check that the current engine was saved. + let metadata = await promiseGlobalMetadata(); + let currentEngine = Services.search.getEngineByName(kTestEngineName); + Assert.equal(metadata.defaultEngineId, currentEngine.id); + + // Then set the engine back to the default through the API. + await Services.search.setDefault( + Services.search.getEngineByName(kDefaultEngineName), + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await promiseAfterSettings(); + + // Check that the current engine is no longer saved in the JSON file. + metadata = await promiseGlobalMetadata(); + Assert.equal(metadata.defaultEngineId, ""); +}); + +add_task(async function test_resetToOriginalDefaultEngine() { + Assert.equal(Services.search.defaultEngine.name, kDefaultEngineName); + + await Services.search.setDefault( + Services.search.getEngineByName(kTestEngineName), + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + Assert.equal(Services.search.defaultEngine.name, kTestEngineName); + await promiseAfterSettings(); + + Services.search.resetToAppDefaultEngine(); + Assert.equal(Services.search.defaultEngine.name, kDefaultEngineName); + await promiseAfterSettings(); +}); + +add_task(async function test_fallback_kept_after_restart() { + // Set current engine to a default engine that isn't the original default. + let builtInEngines = await Services.search.getAppProvidedEngines(); + let nonDefaultBuiltInEngine; + for (let engine of builtInEngines) { + if (engine.name != kDefaultEngineName) { + nonDefaultBuiltInEngine = engine; + break; + } + } + await Services.search.setDefault( + nonDefaultBuiltInEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + Assert.equal( + Services.search.defaultEngine.name, + nonDefaultBuiltInEngine.name + ); + await promiseAfterSettings(); + + // Remove that engine... + await Services.search.removeEngine(nonDefaultBuiltInEngine); + // The engine being a default (built-in) one, it should be hidden + // rather than actually removed. + Assert.ok(nonDefaultBuiltInEngine.hidden); + + // Using the defaultEngine getter should force a fallback to the + // original default engine. + Assert.equal(Services.search.defaultEngine.name, kDefaultEngineName); + + // Restoring the default engines should unhide our built-in test + // engine, but not change the value of defaultEngine. + Services.search.restoreDefaultEngines(); + Assert.ok(!nonDefaultBuiltInEngine.hidden); + Assert.equal(Services.search.defaultEngine.name, kDefaultEngineName); + await promiseAfterSettings(); + + // After a restart, the defaultEngine value should still be unchanged. + Services.search.wrappedJSObject.reset(); + await Services.search.init(true); + Assert.equal(Services.search.defaultEngine.name, kDefaultEngineName); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_sendSubmissionURL.js b/toolkit/components/search/tests/xpcshell/test_sendSubmissionURL.js new file mode 100644 index 0000000000..0a41a47621 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_sendSubmissionURL.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests covering sending submission URLs for major engines + */ + +const SUBMISSION_YES = [ + ["Google1 Test", "https://www.google.com/search", "q={searchTerms}"], + ["Google2 Test", "https://www.google.co.uk/search", "q={searchTerms}"], + ["Yahoo1 Test", "https://search.yahoo.com/search", "p={searchTerms}"], + ["Yahoo2 Test", "https://uk.search.yahoo.com/search", "p={searchTerms}"], + ["AOL1 Test", "https://search.aol.com/aol/search", "q={searchTerms}"], + ["AOL2 Test", "https://search.aol.co.uk/aol/search", "q={searchTerms}"], + ["Yandex1 Test", "https://yandex.ru/search/", "text={searchTerms}"], + ["Yandex2 Test", "https://yandex.com/search/", "text={searchTerms}"], + ["Ask1 Test", "https://www.ask.com/web", "q={searchTerms}"], + ["Ask2 Test", "https://fr.ask.com/web", "q={searchTerms}"], + ["Bing Test", "https://www.bing.com/search", "q={searchTerms}"], + [ + "Startpage Test", + "https://www.startpage.com/do/search", + "query={searchTerms}", + ], + ["DuckDuckGo Test", "https://duckduckgo.com/", "q={searchTerms}"], + ["Baidu Test", "https://www.baidu.com/s", "wd={searchTerms}"], +]; + +const SUBMISSION_NO = [ + ["Other1 Test", "https://example.com", "q={searchTerms}"], + ["Other2 Test", "https://googlebutnotgoogle.com", "q={searchTerms}"], +]; + +add_setup(async function () { + await SearchTestUtils.useTestEngines("data1"); + await AddonTestUtils.promiseStartupManager(); +}); + +async function addAndMakeDefault(name, search_url, search_url_get_params) { + await SearchTestUtils.installSearchExtension({ + name, + search_url, + search_url_get_params, + }); + + let engine = Services.search.getEngineByName(name); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + return engine; +} + +add_task(async function test_submission_url_matching() { + Assert.ok(!Services.search.isInitialized); + let engineInfo; + let engine; + + for (let [name, searchURL, searchParams] of SUBMISSION_YES) { + engine = await addAndMakeDefault(name, searchURL, searchParams); + engineInfo = Services.search.getDefaultEngineInfo(); + Assert.equal( + engineInfo.defaultSearchEngineData.submissionURL, + (searchURL + "?" + searchParams).replace("{searchTerms}", "") + ); + await Services.search.removeEngine(engine); + } + + for (let [name, searchURL, searchParams] of SUBMISSION_NO) { + engine = await addAndMakeDefault(name, searchURL, searchParams); + engineInfo = Services.search.getDefaultEngineInfo(); + Assert.equal(engineInfo.defaultSearchEngineData.submissionURL, null); + await Services.search.removeEngine(engine); + } +}); + +add_task(async function test_submission_url_built_in() { + const engine = await Services.search.getEngineByName("engine1"); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + const engineInfo = Services.search.getDefaultEngineInfo(); + Assert.equal( + engineInfo.defaultSearchEngineData.submissionURL, + "https://1.example.com/search?q=", + "Should have given the submission url for a built-in engine." + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_settings.js b/toolkit/components/search/tests/xpcshell/test_settings.js new file mode 100644 index 0000000000..33995b49fd --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_settings.js @@ -0,0 +1,612 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test initializing from the search settings. + */ + +"use strict"; + +const legacyUseSavedOrderPrefName = + SearchUtils.BROWSER_SEARCH_PREF + "useDBForOrder"; + +var settingsTemplate; + +/** + * Test reading from search.json.mozlz4 + */ +add_setup(async function () { + await SearchTestUtils.useTestEngines("data1"); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +}); + +async function loadSettingsFile(settingsFile, setVersion, setHashes) { + settingsTemplate = await readJSONFile(do_get_file(settingsFile)); + if (setVersion) { + settingsTemplate.version = SearchUtils.SETTINGS_VERSION; + } + + if (setHashes) { + settingsTemplate.metaData.hash = SearchUtils.getVerificationHash( + settingsTemplate.metaData.current + ); + settingsTemplate.metaData.privateHash = SearchUtils.getVerificationHash( + settingsTemplate.metaData.private + ); + } + + delete settingsTemplate.visibleDefaultEngines; + + await promiseSaveSettingsData(settingsTemplate); +} + +/** + * Start the search service and confirm the engine properties match the expected values. + * + * @param {string} settingsFile + * The path to the settings file to use. + * @param {boolean} setVersion + * True if to set the version in the copied settings file. + * @param {boolean} expectedUseDBValue + * The value expected for the `useSavedOrder` metadata attribute. + */ +async function checkLoadSettingProperties( + settingsFile, + setVersion, + expectedUseDBValue +) { + info("init search service"); + let ss = Services.search.wrappedJSObject; + + await loadSettingsFile(settingsFile, setVersion); + + const settingsFileWritten = promiseAfterSettings(); + + await ss.reset(); + await Services.search.init(); + + await settingsFileWritten; + + let engines = await ss.getEngines(); + + Assert.equal( + engines[0].name, + "engine1", + "Should have loaded the correct first engine" + ); + Assert.equal(engines[0].alias, "testAlias", "Should have set the alias"); + Assert.equal(engines[0].hidden, false, "Should have not hidden the engine"); + Assert.equal(engines[0].id, "engine1@search.mozilla.orgdefault"); + + Assert.equal( + engines[1].name, + "engine2", + "Should have loaded the correct second engine" + ); + Assert.equal(engines[1].alias, "", "Should have not set the alias"); + Assert.equal(engines[1].hidden, true, "Should have hidden the engine"); + Assert.equal(engines[1].id, "engine2@search.mozilla.orgdefault"); + + // The extra engine is the second in the list. + isSubObjectOf(EXPECTED_ENGINE.engine, engines[2]); + Assert.ok(engines[2].id, "test-addon-id@mozilla.orgdefault"); + + let engineFromSS = ss.getEngineByName(EXPECTED_ENGINE.engine.name); + Assert.ok(!!engineFromSS); + isSubObjectOf(EXPECTED_ENGINE.engine, engineFromSS); + + Assert.equal( + engineFromSS.getSubmission("foo").uri.spec, + "http://www.google.com/search?q=foo", + "Should have the correct URL with no mozparams" + ); + + Assert.equal( + ss._settings.getMetaDataAttribute("useSavedOrder"), + expectedUseDBValue, + "Should have set the useSavedOrder metadata correctly." + ); + + let migratedSettingsFile = await promiseSettingsData(); + + Assert.equal( + migratedSettingsFile.engines[0].id, + "engine1@search.mozilla.orgdefault" + ); + + removeSettingsFile(); +} + +add_task(async function test_legacy_setting_engine_properties() { + Services.prefs.setBoolPref(legacyUseSavedOrderPrefName, true); + + let legacySettings = await readJSONFile( + do_get_file("data/search-legacy.json") + ); + + // Assert the engine ids have not been migrated yet + for (let engine of legacySettings.engines) { + Assert.ok(!("id" in engine)); + } + Assert.ok(!("defaultEngineId" in legacySettings.metaData)); + Assert.ok(!("privateDefaultEngineId" in legacySettings.metaData)); + + await checkLoadSettingProperties("data/search-legacy.json", false, true); + + Assert.ok( + !Services.prefs.prefHasUserValue(legacyUseSavedOrderPrefName), + "Should have cleared the legacy pref." + ); +}); + +add_task( + async function test_legacy_setting_migration_with_undefined_metaData_current_and_private() { + let ss = Services.search.wrappedJSObject; + + await loadSettingsFile("data/search-legacy.json", false); + const settingsFileWritten = promiseAfterSettings(); + + await ss.reset(); + await Services.search.init(); + + await settingsFileWritten; + + let migratedSettingsFile = await promiseSettingsData(); + + Assert.equal( + migratedSettingsFile.metaData.defaultEngineId, + "", + "When there is no metaData.current attribute in settings file, the migration should set the defaultEngineId to an empty string." + ); + Assert.equal( + migratedSettingsFile.metaData.privateDefaultEngineId, + "", + "When there is no metaData.private attribute in settings file, the migration should set the privateDefaultEngineId to an empty string." + ); + + removeSettingsFile(); + } +); + +add_task( + async function test_legacy_setting_migration_with_correct_metaData_current_and_private_hashes() { + let ss = Services.search.wrappedJSObject; + + await loadSettingsFile( + "data/search-legacy-correct-default-engine-hashes.json", + false, + true + ); + const settingsFileWritten = promiseAfterSettings(); + + await ss.reset(); + await Services.search.init(); + + await settingsFileWritten; + + let migratedSettingsFile = await promiseSettingsData(); + + Assert.equal( + migratedSettingsFile.metaData.defaultEngineId, + "engine2@search.mozilla.orgdefault", + "When the metaData.current and associated hash are correct, the migration should set the defaultEngineId to the engine id." + ); + Assert.equal( + migratedSettingsFile.metaData.privateDefaultEngineId, + "engine2@search.mozilla.orgdefault", + "When the metaData.private and associated hash are correct, the migration should set the privateDefaultEngineId to the private engine id." + ); + + removeSettingsFile(); + } +); + +add_task( + async function test_legacy_setting_migration_with_incorrect_metaData_current_and_private_hashes_app_provided() { + let ss = Services.search.wrappedJSObject; + + // Here we are testing correct migration for the case that a user has set + // their default engine to an application provided engine (but not the app + // default). + // + // In this case we should ignore invalid hashes for the default engines, + // and allow the select default to remain. This covers the case where + // a user has copied a profile from a different directory. + // See SearchService._getEngineDefault for more details. + + await loadSettingsFile( + "data/search-legacy-wrong-default-engine-hashes.json", + false, + false + ); + const settingsFileWritten = promiseAfterSettings(); + + await ss.reset(); + await Services.search.init(); + + await settingsFileWritten; + + let migratedSettingsFile = await promiseSettingsData(); + + Assert.equal( + migratedSettingsFile.metaData.defaultEngineId, + "engine2@search.mozilla.orgdefault", + "Should ignore invalid metaData.hash when the default engine is application provided." + ); + Assert.equal( + Services.search.defaultEngine.name, + "engine2", + "Should have the correct engine set as default" + ); + + Assert.equal( + migratedSettingsFile.metaData.privateDefaultEngineId, + "engine2@search.mozilla.orgdefault", + "Should ignore invalid metaData.privateHash when the default private engine is application provided." + ); + Assert.equal( + Services.search.defaultPrivateEngine.name, + "engine2", + "Should have the correct engine set as default private" + ); + + removeSettingsFile(); + } +); + +add_task( + async function test_legacy_setting_migration_with_incorrect_metaData_current_and_private_hashes_third_party() { + let ss = Services.search.wrappedJSObject; + + // This test is checking that if the user has set a third-party engine as + // default, and the verification hash is invalid, then we do not copy + // the default engine setting. + + await loadSettingsFile( + "data/search-legacy-wrong-third-party-engine-hashes.json", + false, + false + ); + const settingsFileWritten = promiseAfterSettings(); + + await ss.reset(); + await Services.search.init(); + + await settingsFileWritten; + + let migratedSettingsFile = await promiseSettingsData(); + + Assert.equal( + migratedSettingsFile.metaData.defaultEngineId, + "", + "Should reset the default engine when metaData.hash is invalid and the engine is not application provided." + ); + Assert.equal( + Services.search.defaultEngine.name, + "engine1", + "Should have reset the default engine" + ); + + Assert.equal( + migratedSettingsFile.metaData.privateDefaultEngineId, + "", + "Should reset the default engine when metaData.privateHash is invalid and the engine is not application provided." + ); + Assert.equal( + Services.search.defaultPrivateEngine.name, + "engine1", + "Should have reset the default private engine" + ); + + removeSettingsFile(); + } +); + +add_task(async function test_current_setting_engine_properties() { + await checkLoadSettingProperties("data/search.json", true, false); +}); + +add_task(async function test_settings_metadata_properties() { + let ss = Services.search.wrappedJSObject; + + await loadSettingsFile("data/search.json"); + + const settingsFileWritten = promiseAfterSettings(); + await ss.reset(); + await Services.search.init(); + + await settingsFileWritten; + + let metaDataProperties = [ + "locale", + "region", + "channel", + "experiment", + "distroID", + ]; + + for (let name of metaDataProperties) { + Assert.notEqual( + ss._settings.getMetaDataAttribute(`${name}`), + undefined, + `Search settings should have ${name} property defined.` + ); + } + + removeSettingsFile(); +}); + +add_task(async function test_settings_write_when_settings_changed() { + let ss = Services.search.wrappedJSObject; + await loadSettingsFile("data/search.json"); + + const settingsFileWritten = promiseAfterSettings(); + await ss.reset(); + await Services.search.init(); + await settingsFileWritten; + + Assert.ok( + ss._settings.isCurrentAndCachedSettingsEqual(), + "Settings and cached settings should be the same after search service initializaiton." + ); + + const settingsFileWritten2 = promiseAfterSettings(); + ss._settings.setMetaDataAttribute("value", "test"); + + Assert.ok( + !ss._settings.isCurrentAndCachedSettingsEqual(), + "Settings should differ from cached settings after a new attribute is set." + ); + + await settingsFileWritten2; + info("Settings write complete"); + + Assert.ok( + ss._settings.isCurrentAndCachedSettingsEqual(), + "Settings and cached settings should be the same after new attribte on settings is written." + ); + + removeSettingsFile(); +}); + +add_task(async function test_set_and_get_engine_metadata_attribute() { + let ss = Services.search.wrappedJSObject; + await loadSettingsFile("data/search.json"); + + const settingsFileWritten = promiseAfterSettings(); + await ss.reset(); + await Services.search.init(); + await settingsFileWritten; + + let engines = await ss.getEngines(); + const settingsFileWritten2 = promiseAfterSettings(); + ss._settings.setEngineMetaDataAttribute(engines[0].name, "value", "test"); + await settingsFileWritten2; + + Assert.equal( + "test", + ss._settings.getEngineMetaDataAttribute(engines[0].name, "value"), + `${engines[0].name}'s metadata property "value" should be set as "test" after calling getEngineMetaDataAttribute.` + ); + + let userSettings = await ss._settings.get(); + let engine = userSettings.engines.find(e => e._name == engines[0].name); + + Assert.equal( + "test", + engine._metaData.value, + `${engines[0].name}'s metadata property "value" should be set as "test" from settings file.` + ); + + removeSettingsFile(); +}); + +add_task( + async function test_settings_write_prevented_when_settings_unchanged() { + let ss = Services.search.wrappedJSObject; + await loadSettingsFile("data/search.json"); + + const settingsFileWritten = promiseAfterSettings(); + await ss.reset(); + await Services.search.init(); + await settingsFileWritten; + + Assert.ok( + ss._settings.isCurrentAndCachedSettingsEqual(), + "Settings and cached settings should be the same after search service initializaiton." + ); + + // Update settings. + const settingsFileWritten2 = promiseAfterSettings(); + ss._settings.setMetaDataAttribute("value", "test"); + + Assert.ok( + !ss._settings.isCurrentAndCachedSettingsEqual(), + "Settings should differ from cached settings after a new attribute is set." + ); + await settingsFileWritten2; + + // Set the same attribute as before to ensure there was no change. + // Settings write should be prevented. + let promiseWritePrevented = SearchTestUtils.promiseSearchNotification( + "write-prevented-when-settings-unchanged" + ); + ss._settings.setMetaDataAttribute("value", "test"); + + Assert.ok( + ss._settings.isCurrentAndCachedSettingsEqual(), + "Settings and cached settings should be the same." + ); + await promiseWritePrevented; + + removeSettingsFile(); + } +); + +/** + * Test that the JSON settings written in the profile is correct. + */ +add_task(async function test_settings_write() { + let ss = Services.search.wrappedJSObject; + info("test settings writing"); + + await loadSettingsFile("data/search.json"); + + const settingsFileWritten = promiseAfterSettings(); + await ss.reset(); + await Services.search.init(); + await settingsFileWritten; + + let settingsData = await promiseSettingsData(); + + // Remove buildID and locale, as they are no longer used. + delete settingsTemplate.buildID; + delete settingsTemplate.locale; + + for (let engine of settingsTemplate.engines) { + // Remove _shortName from the settings template, as it is no longer supported, + // but older settings used to have it, so we keep it in the template as an + // example. + if ("_shortName" in engine) { + delete engine._shortName; + } + if ("_urls" in engine) { + // Only app-provided engines support purpose, others do not, + // so filter them out of the expected template. + for (let urls of engine._urls) { + urls.params = urls.params.filter(p => !p.purpose); + // resultDomain is also no longer supported. + if ("resultDomain" in urls) { + delete urls.resultDomain; + } + } + } + // Remove queryCharset, if it is the same as the default, as we don't save + // it in that case. + if (engine?.queryCharset == SearchUtils.DEFAULT_QUERY_CHARSET) { + delete engine.queryCharset; + } + } + + // Note: the file is copied with an old version number, which should have + // been updated on write. + settingsTemplate.version = SearchUtils.SETTINGS_VERSION; + + isSubObjectOf(settingsTemplate, settingsData, (prop, value) => { + if (prop != "_iconURL" && prop != "{}") { + return false; + } + // Skip items that are to do with icons for extensions, as we can't + // control the uuid. + return value.startsWith("moz-extension://"); + }); +}); + +async function settings_write_check(disableFn) { + let ss = Services.search.wrappedJSObject; + + sinon.stub(ss._settings, "_write").returns(Promise.resolve()); + + // Simulate the search service being initialized. + disableFn(true); + + ss._settings.setMetaDataAttribute("value", "test"); + + Assert.ok( + ss._settings._write.notCalled, + "Should not have attempted to _write" + ); + + // Wait for two periods of the normal delay to ensure we still do not write. + await new Promise(r => + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(r, SearchSettings.SETTNGS_INVALIDATION_DELAY * 2) + ); + + Assert.ok( + ss._settings._write.notCalled, + "Should not have attempted to _write" + ); + + disableFn(false); + + await TestUtils.waitForCondition( + () => ss._settings._write.calledOnce, + "Should attempt to write the settings." + ); + + sinon.restore(); +} + +add_task(async function test_settings_write_prevented_during_init() { + await settings_write_check(disable => { + let status = disable ? "success" : "failed"; + Services.search.wrappedJSObject.forceInitializationStatusForTests(status); + }); +}); + +add_task(async function test_settings_write_prevented_during_reload() { + await settings_write_check( + disable => (Services.search.wrappedJSObject._reloadingEngines = disable) + ); +}); + +var EXPECTED_ENGINE = { + engine: { + name: "Test search engine", + alias: "", + description: "A test search engine (based on Google search)", + searchForm: "http://www.google.com/", + wrappedJSObject: { + _extensionID: "test-addon-id@mozilla.org", + _iconURL: + "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAA" + + "AIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9F" + + "sfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2F" + + "Ptft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2Fgg" + + "M%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F" + + "%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJ" + + "vvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%" + + "2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%" + + "2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%" + + "2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%" + + "2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYS" + + "BHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWc" + + "TxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4j" + + "wA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsgg" + + "A7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7" + + "kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%" + + "2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFE" + + "MwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%" + + "2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCT" + + "IYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesA" + + "AN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOc" + + "AAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v" + + "8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA", + _urls: [ + { + type: "application/x-suggestions+json", + method: "GET", + template: + "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox" + + "&hl={moz:locale}&q={searchTerms}", + params: "", + }, + { + type: "text/html", + method: "GET", + template: "http://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + purpose: undefined, + }, + ], + }, + ], + }, + }, +}; diff --git a/toolkit/components/search/tests/xpcshell/test_settings_broken.js b/toolkit/components/search/tests/xpcshell/test_settings_broken.js new file mode 100644 index 0000000000..dcc197d802 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_settings_broken.js @@ -0,0 +1,131 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test initializing from broken search settings. This is one where the engines + * array for some reason has lost all the default engines, but retained either + * one or two, or a user-supplied engine. We don't know why this happens, but + * we have seen it (bug 1578807). + */ + +"use strict"; + +const { getAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); + +const enginesSettings = { + version: SearchUtils.SETTINGS_VERSION, + buildID: "TBD", + appVersion: "TBD", + locale: "en-US", + metaData: { + searchDefault: "Test search engine", + searchDefaultHash: "TBD", + // Intentionally in the past, but shouldn't actually matter for this test. + searchDefaultExpir: 1567694909002, + current: "", + hash: "TBD", + visibleDefaultEngines: + "engine,engine-pref,engine-rel-searchform-purpose,engine-chromeicon,engine-resourceicon,engine-reordered", + visibleDefaultEnginesHash: "TBD", + }, + engines: [ + // This is a user-installed engine - the only one that was listed due to the + // original issue. + { + _name: "A second test engine", + _shortName: "engine2", + _loadPath: "[profile]/searchplugins/engine2.xml", + description: "A second test search engine (based on DuckDuckGo)", + _iconURL: + "data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAEAgQAhIOEAMjHyABIR0gA6ejpAGlqaQCpqKkAKCgoAPz9/AAZGBkAmJiYANjZ2ABXWFcAent6ALm6uQA8OjwAiIiIiIiIiIiIiI4oiL6IiIiIgzuIV4iIiIhndo53KIiIiB/WvXoYiIiIfEZfWBSIiIEGi/foqoiIgzuL84i9iIjpGIoMiEHoiMkos3FojmiLlUipYliEWIF+iDe0GoRa7D6GPbjcu1yIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + _iconMapObj: { + '{"width":16,"height":16}': + "data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAEAgQAhIOEAMjHyABIR0gA6ejpAGlqaQCpqKkAKCgoAPz9/AAZGBkAmJiYANjZ2ABXWFcAent6ALm6uQA8OjwAiIiIiIiIiIiIiI4oiL6IiIiIgzuIV4iIiIhndo53KIiIiB/WvXoYiIiIfEZfWBSIiIEGi/foqoiIgzuL84i9iIjpGIoMiEHoiMkos3FojmiLlUipYliEWIF+iDe0GoRa7D6GPbjcu1yIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + }, + _isBuiltin: false, + _metaData: { + order: 1, + }, + _urls: [ + { + template: "https://duckduckgo.com/?q={searchTerms}", + rels: [], + resultDomain: "duckduckgo.com", + params: [], + }, + ], + queryCharset: "UTF-8", + filePath: "TBD", + }, + ], +}; + +add_setup(async function () { + await AddonTestUtils.promiseStartupManager(); + + // Allow telemetry probes which may otherwise be disabled for some applications (e.g. Thunderbird) + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + await SearchTestUtils.useTestEngines(); + Services.prefs.setCharPref(SearchUtils.BROWSER_SEARCH_PREF + "region", "US"); + Services.locale.availableLocales = ["en-US"]; + Services.locale.requestedLocales = ["en-US"]; + + // We dynamically generate the hashes because these depend on the profile. + enginesSettings.metaData.searchDefaultHash = SearchUtils.getVerificationHash( + enginesSettings.metaData.searchDefault + ); + enginesSettings.metaData.hash = SearchUtils.getVerificationHash( + enginesSettings.metaData.current + ); + enginesSettings.metaData.visibleDefaultEnginesHash = + SearchUtils.getVerificationHash( + enginesSettings.metaData.visibleDefaultEngines + ); + const appInfo = getAppInfo(); + enginesSettings.buildID = appInfo.platformBuildID; + enginesSettings.appVersion = appInfo.version; + + await IOUtils.writeJSON( + PathUtils.join(PathUtils.profileDir, SETTINGS_FILENAME), + enginesSettings, + { compress: true } + ); +}); + +add_task(async function test_cached_engine_properties() { + info("init search service"); + + const initResult = await Services.search.init(); + + info("init'd search service"); + Assert.ok( + Components.isSuccessCode(initResult), + "Should have successfully created the search service" + ); + + const engines = await Services.search.getEngines(); + + const expectedEngines = [ + // Default engines + "Test search engine", + // Rest of engines in order + "engine-resourceicon", + "engine-chromeicon", + "engine-pref", + "engine-rel-searchform-purpose", + "Test search engine (Reordered)", + "A second test engine", + ]; + + Assert.deepEqual( + engines.map(e => e.name), + expectedEngines, + "Should have the expected default engines" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_settings_duplicate.js b/toolkit/components/search/tests/xpcshell/test_settings_duplicate.js new file mode 100644 index 0000000000..20093be6a0 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_settings_duplicate.js @@ -0,0 +1,146 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test initializing with an engine that's a duplicate of an app-provided + * engine. + */ + +"use strict"; + +const { getAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); + +const DUPLICATE_ENGINE_ID = "f3094ab7-3302-4d5b-9f79-ea92c9a49f87"; + +const enginesSettings = { + version: SearchUtils.SETTINGS_VERSION, + buildID: "TBD", + appVersion: "TBD", + locale: "en-US", + metaData: { + searchDefault: "Test search engine", + searchDefaultHash: "TBD", + // Intentionally in the past, but shouldn't actually matter for this test. + searchDefaultExpir: 1567694909002, + current: "", + hash: "TBD", + visibleDefaultEngines: + "engine,engine-pref,engine-rel-searchform-purpose,engine-chromeicon,engine-resourceicon,engine-reordered", + visibleDefaultEnginesHash: "TBD", + }, + engines: [ + { + id: "engine1@search.mozilla.orgdefault", + _metaData: { alias: null }, + _isAppProvided: true, + _name: "engine1", + }, + { + id: "engine2@search.mozilla.orgdefault", + _metaData: { alias: null }, + _isAppProvided: true, + _name: "engine2", + }, + // This is a user-installed engine - the only one that was listed due to the + // original issue. + { + id: DUPLICATE_ENGINE_ID, + _name: "engine1", + _shortName: "engine1", + _loadPath: "[https]oldduplicateversion", + description: "An old near duplicate version of engine1", + _iconURL: + "data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAEAgQAhIOEAMjHyABIR0gA6ejpAGlqaQCpqKkAKCgoAPz9/AAZGBkAmJiYANjZ2ABXWFcAent6ALm6uQA8OjwAiIiIiIiIiIiIiI4oiL6IiIiIgzuIV4iIiIhndo53KIiIiB/WvXoYiIiIfEZfWBSIiIEGi/foqoiIgzuL84i9iIjpGIoMiEHoiMkos3FojmiLlUipYliEWIF+iDe0GoRa7D6GPbjcu1yIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + _iconMapObj: { + '{"width":16,"height":16}': + "data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAEAgQAhIOEAMjHyABIR0gA6ejpAGlqaQCpqKkAKCgoAPz9/AAZGBkAmJiYANjZ2ABXWFcAent6ALm6uQA8OjwAiIiIiIiIiIiIiI4oiL6IiIiIgzuIV4iIiIhndo53KIiIiB/WvXoYiIiIfEZfWBSIiIEGi/foqoiIgzuL84i9iIjpGIoMiEHoiMkos3FojmiLlUipYliEWIF+iDe0GoRa7D6GPbjcu1yIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + }, + _metaData: { + order: 1, + }, + _urls: [ + { + template: "https://example.com/?myquery={searchTerms}", + rels: [], + resultDomain: "example.com", + params: [], + }, + ], + queryCharset: "UTF-8", + filePath: "TBD", + }, + ], +}; + +add_setup(async function () { + await AddonTestUtils.promiseStartupManager(); + + // Allow telemetry probes which may otherwise be disabled for some applications (e.g. Thunderbird) + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + await SearchTestUtils.useTestEngines("data1"); + Services.prefs.setCharPref(SearchUtils.BROWSER_SEARCH_PREF + "region", "US"); + Services.locale.availableLocales = ["en-US"]; + Services.locale.requestedLocales = ["en-US"]; + + // We dynamically generate the hashes because these depend on the profile. + enginesSettings.metaData.searchDefaultHash = SearchUtils.getVerificationHash( + enginesSettings.metaData.searchDefault + ); + enginesSettings.metaData.hash = SearchUtils.getVerificationHash( + enginesSettings.metaData.current + ); + enginesSettings.metaData.visibleDefaultEnginesHash = + SearchUtils.getVerificationHash( + enginesSettings.metaData.visibleDefaultEngines + ); + let appInfo = getAppInfo(); + enginesSettings.buildID = appInfo.platformBuildID; + enginesSettings.appVersion = appInfo.version; + + await IOUtils.write( + PathUtils.join(PathUtils.profileDir, SETTINGS_FILENAME), + new TextEncoder().encode(JSON.stringify(enginesSettings)), + { compress: true } + ); + + consoleAllowList.push("Failed to load"); +}); + +add_task(async function test_cached_duplicate() { + info("init search service"); + + let initResult = await Services.search.init(); + + info("init'd search service"); + Assert.ok( + Components.isSuccessCode(initResult), + "Should have successfully created the search service" + ); + + let engine = Services.search.getEngineByName("engine1"); + let submission = engine.getSubmission("foo"); + Assert.equal( + submission.uri.spec, + "https://1.example.com/search?q=foo", + "Should have not changed the app provided engine." + ); + + Assert.ok( + !(await Services.search.getEngineById(DUPLICATE_ENGINE_ID)), + "Should not have added the duplicate engine" + ); + + let engines = await Services.search.getEngines(); + + Assert.deepEqual( + engines.map(e => e.name), + ["engine1", "engine2"], + "Should have the expected default engines" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_settings_good.js b/toolkit/components/search/tests/xpcshell/test_settings_good.js new file mode 100644 index 0000000000..b954b94038 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_settings_good.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test initializing from good search settings. + */ + +"use strict"; + +const { getAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); + +const enginesSettings = { + version: SearchUtils.SETTINGS_VERSION, + buildID: "TBD", + appVersion: "TBD", + locale: "en-US", + metaData: { + searchDefault: "Test search engine", + searchDefaultHash: "TBD", + // Intentionally in the past, but shouldn't actually matter for this test. + searchDefaultExpir: 1567694909002, + // We use the second engine here so that the user's default is set + // to something different, and hence so that we exercise the appropriate + // code paths. + defaultEngineId: "engine2@search.mozilla.orgdefault", + defaultEngineIdHash: "TBD", + visibleDefaultEngines: "engine1,engine2", + visibleDefaultEnginesHash: "TBD", + }, + engines: [ + { + _metaData: { alias: null }, + _isAppProvided: true, + _name: "engine1", + }, + { + _metaData: { alias: null }, + _isAppProvided: true, + _name: "engine2", + }, + ], +}; + +add_setup(async function () { + await AddonTestUtils.promiseStartupManager(); + + // Allow telemetry probes which may otherwise be disabled for some applications (e.g. Thunderbird) + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + await SearchTestUtils.useTestEngines("data1"); + Services.prefs.setCharPref(SearchUtils.BROWSER_SEARCH_PREF + "region", "US"); + Services.locale.availableLocales = ["en-US"]; + Services.locale.requestedLocales = ["en-US"]; + + // We dynamically generate the hashes because these depend on the profile. + enginesSettings.metaData.searchDefaultHash = SearchUtils.getVerificationHash( + enginesSettings.metaData.searchDefault + ); + enginesSettings.metaData.defaultEngineIdHash = + SearchUtils.getVerificationHash(enginesSettings.metaData.defaultEngineId); + enginesSettings.metaData.visibleDefaultEnginesHash = + SearchUtils.getVerificationHash( + enginesSettings.metaData.visibleDefaultEngines + ); + const appInfo = getAppInfo(); + enginesSettings.buildID = appInfo.platformBuildID; + enginesSettings.appVersion = appInfo.version; + + await IOUtils.writeJSON( + PathUtils.join(PathUtils.profileDir, SETTINGS_FILENAME), + enginesSettings, + { compress: true } + ); +}); + +add_task(async function test_cached_engine_properties() { + info("init search service"); + + const initResult = await Services.search.init(); + + info("init'd search service"); + Assert.ok( + Components.isSuccessCode(initResult), + "Should have successfully created the search service" + ); + + const engines = await Services.search.getEngines(); + Assert.equal( + Services.search.defaultEngine.name, + "engine2", + "Should have the expected default engine" + ); + Assert.deepEqual( + engines.map(e => e.name), + ["engine1", "engine2"], + "Should have the expected application provided engines" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_settings_ignorelist.js b/toolkit/components/search/tests/xpcshell/test_settings_ignorelist.js new file mode 100644 index 0000000000..edc8eb12b8 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_settings_ignorelist.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test initializing from the search settings. + */ + +"use strict"; + +var { getAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); + +var settingsTemplate; + +/** + * Test reading from search.json.mozlz4 + */ +add_setup(async function () { + await AddonTestUtils.promiseStartupManager(); + + await setupRemoteSettings(); + + settingsTemplate = await readJSONFile( + do_get_file("data/search_ignorelist.json") + ); + settingsTemplate.buildID = getAppInfo().platformBuildID; + + await promiseSaveSettingsData(settingsTemplate); +}); + +/** + * Start the search service and confirm the settings were reset + */ +add_task(async function test_settings_rest() { + info("init search service"); + + let updatePromise = SearchTestUtils.promiseSearchNotification( + "settings-update-complete" + ); + + let result = await Services.search.init(); + + Assert.ok( + Components.isSuccessCode(result), + "Search service should be successfully initialized" + ); + await updatePromise; + + const engines = await Services.search.getEngines(); + + // Engine list will have been reset to the default, + // Not the one engine in the settings. + // It should have more than one engine. + Assert.greater( + engines.length, + 1, + "Should have more than one engine in the list" + ); + + removeSettingsFile(); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_settings_migration_hideOneOffs.js b/toolkit/components/search/tests/xpcshell/test_settings_migration_hideOneOffs.js new file mode 100644 index 0000000000..a35ef367d7 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_settings_migration_hideOneOffs.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Loads the settings file and ensures it has not already been migrated. + * + * @param {string} settingsFile The settings file to load + */ +async function loadSettingsFile(settingsFile) { + let settingsTemplate = await readJSONFile(do_get_file(settingsFile)); + + Assert.less( + settingsTemplate.version, + 7, + "Should be a version older than when hideOneOffs was moved into settings" + ); + for (let engine of settingsTemplate.engines) { + Assert.ok(!("id" in engine)); + } + + await promiseSaveSettingsData(settingsTemplate); +} + +/** + * Test reading from search.json.mozlz4 + */ +add_setup(async function () { + await SearchTestUtils.useTestEngines("data1"); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +}); + +add_task(async function test_migration_from_pre_ids() { + await loadSettingsFile("data/search-legacy.json"); + + Services.prefs.setStringPref("browser.search.hiddenOneOffs", "engine1"); + + const settingsFileWritten = promiseAfterSettings(); + + await Services.search.wrappedJSObject.reset(); + await Services.search.init(); + + await settingsFileWritten; + + const engine1 = await Services.search.getEngineByName("engine1"); + const engine2 = await Services.search.getEngineByName("engine2"); + + Assert.ok( + engine1.hideOneOffButton, + "Should have hideOneOffButton set to true" + ); + Assert.ok( + !engine2.hideOneOffButton, + "Should have hideOneOffButton set to false" + ); + + Assert.ok( + !Services.prefs.prefHasUserValue("browser.search.hiddenOneOffs"), + "HiddenOneOffs pref is cleared" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_settings_migration_ids.js b/toolkit/components/search/tests/xpcshell/test_settings_migration_ids.js new file mode 100644 index 0000000000..8123ccc4ab --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_settings_migration_ids.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test migration of user, enterprise policy and OpenSearch engines + * from when engines were referenced by name rather than id. + * + * Add-ons and default engine ids are tested in test_settings.js. + */ + +"use strict"; + +const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" +); + +const enterprisePolicy = { + policies: { + SearchEngines: { + Add: [ + { + Name: "Policy", + Encoding: "windows-1252", + URLTemplate: "http://example.com/?q={searchTerms}", + }, + ], + }, + }, +}; + +/** + * Loads the settings file and ensures it has not already been migrated. + * + * @param {string} settingsFile The settings file to load + */ +async function loadSettingsFile(settingsFile) { + let settingsTemplate = await readJSONFile(do_get_file(settingsFile)); + + Assert.less( + settingsTemplate.version, + 7, + "Should be a version older than when indexing engines by id was introduced" + ); + for (let engine of settingsTemplate.engines) { + Assert.ok(!("id" in engine)); + } + + await promiseSaveSettingsData(settingsTemplate); +} + +/** + * Test reading from search.json.mozlz4 + */ +add_setup(async function () { + // This initializes the policy engine for xpcshell tests + let policies = Cc["@mozilla.org/enterprisepolicies;1"].getService( + Ci.nsIObserver + ); + policies.observe(null, "policies-startup", null); + + await SearchTestUtils.useTestEngines("data1"); + await AddonTestUtils.promiseStartupManager(); + await EnterprisePolicyTesting.setupPolicyEngineWithJson(enterprisePolicy); + // Setting the enterprise policy starts the search service initialising, + // so we wait for that to complete before starting the test. + await Services.search.init(); +}); + +/** + * Tests that an installed engine matches the expected data. + * + * @param {object} expectedData The expected data for the engine + */ +async function assertInstalledEngineMatches(expectedData) { + let engine = await Services.search.getEngineByName(expectedData.name); + + Assert.ok(engine, `Should have found the ${expectedData.type} engine`); + if (expectedData.idLength) { + Assert.equal( + engine.id.length, + expectedData.idLength, + "Should have been given an id" + ); + } else { + Assert.equal(engine.id, expectedData.id, "Should have the expected id"); + } + Assert.equal(engine.alias, expectedData.alias, "Should have kept the alias"); +} + +add_task(async function test_migration_from_pre_ids() { + await loadSettingsFile("data/search-legacy-no-ids.json"); + + const settingsFileWritten = promiseAfterSettings(); + + await Services.search.wrappedJSObject.reset(); + await Services.search.init(); + + await settingsFileWritten; + + await assertInstalledEngineMatches({ + type: "OpenSearch", + name: "Bugzilla@Mozilla", + idLength: 36, + alias: "bugzillaAlias", + }); + await assertInstalledEngineMatches({ + type: "Enterprise Policy", + name: "Policy", + id: "policy-Policy", + alias: "PolicyAlias", + }); + await assertInstalledEngineMatches({ + type: "User", + name: "User", + idLength: 36, + alias: "UserAlias", + }); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_settings_migration_loadPath.js b/toolkit/components/search/tests/xpcshell/test_settings_migration_loadPath.js new file mode 100644 index 0000000000..d4193f7a17 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_settings_migration_loadPath.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test migration load path for user, enterprise policy and add-on + * engines. + */ + +"use strict"; + +const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" +); + +const enterprisePolicy = { + policies: { + SearchEngines: { + Add: [ + { + Name: "Policy", + Encoding: "windows-1252", + URLTemplate: "http://example.com/?q={searchTerms}", + }, + ], + }, + }, +}; + +add_setup(async function () { + // This initializes the policy engine for xpcshell tests + let policies = Cc["@mozilla.org/enterprisepolicies;1"].getService( + Ci.nsIObserver + ); + policies.observe(null, "policies-startup", null); + + await SearchTestUtils.useTestEngines("data1"); + await AddonTestUtils.promiseStartupManager(); + await EnterprisePolicyTesting.setupPolicyEngineWithJson(enterprisePolicy); + // Setting the enterprise policy starts the search service initialising, + // so we wait for that to complete before starting the test, we can + // then also add an extra add-on engine. + await Services.search.init(); + let settingsFileWritten = promiseAfterSettings(); + await SearchTestUtils.installSearchExtension(); + await settingsFileWritten; +}); + +/** + * Loads the settings file and ensures it has not already been migrated. + */ +add_task(async function test_load_and_check_settings() { + let settingsTemplate = await readJSONFile( + do_get_file("data/search-legacy-old-loadPaths.json") + ); + + Assert.less( + settingsTemplate.version, + 8, + "Should be a version older than when indexing engines by id was introduced" + ); + let engine = settingsTemplate.engines.find(e => e.id == "policy-Policy"); + Assert.equal( + engine._loadPath, + "[other]addEngineWithDetails:set-via-policy", + "Should have a old style load path for the policy engine" + ); + engine = settingsTemplate.engines.find( + e => e.id == "bbc163e7-7b1a-47aa-a32c-c59062de2754" + ); + Assert.equal( + engine._loadPath, + "[other]addEngineWithDetails:set-via-user", + "Should have a old style load path for the user engine" + ); + engine = settingsTemplate.engines.find( + e => e.id == "example@tests.mozilla.orgdefault" + ); + Assert.equal( + engine._loadPath, + "[other]addEngineWithDetails:example@tests.mozilla.org", + "Should have a old style load path for the add-on engine" + ); + + await promiseSaveSettingsData(settingsTemplate); +}); + +/** + * Tests that an installed engine matches the expected data. + * + * @param {object} expectedData The expected data for the engine + */ +async function assertInstalledEngineMatches(expectedData) { + let engine = await Services.search.getEngineByName(expectedData.name); + + Assert.ok(engine, `Should have found the ${expectedData.type} engine`); + Assert.equal( + engine.wrappedJSObject._loadPath, + expectedData.loadPath, + "Should have migrated the loadPath" + ); +} + +add_task(async function test_migration_from_pre_ids() { + const settingsFileWritten = promiseAfterSettings(); + + await Services.search.wrappedJSObject.reset(); + await Services.search.init(); + + await settingsFileWritten; + + await assertInstalledEngineMatches({ + type: "Policy", + name: "Policy", + loadPath: "[policy]", + }); + await assertInstalledEngineMatches({ + type: "User", + name: "User", + loadPath: "[user]", + }); + await assertInstalledEngineMatches({ + type: "Add-on", + name: "Example", + loadPath: "[addon]example@tests.mozilla.org", + }); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_settings_none.js b/toolkit/components/search/tests/xpcshell/test_settings_none.js new file mode 100644 index 0000000000..dbd42f2ee5 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_settings_none.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * test_nosettings: Start search engine + * - without search.json.mozlz4 + * + * Ensure that : + * - nothing explodes; + * - search.json.mozlz4 is created. + */ + +add_setup(async function () { + useHttpServer(); + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_nosettings() { + let search = Services.search; + + let afterSettingsPromise = promiseAfterSettings(); + + await search.init(); + + // Check that the settings is created at startup + await afterSettingsPromise; + + // Check that search.json.mozlz4 has been created. + let settingsFile = do_get_profile().clone(); + settingsFile.append(SETTINGS_FILENAME); + Assert.ok(settingsFile.exists()); + + await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}engine.xml`, + }); + + info("Engine has been added, let's wait for the settings to be built"); + await promiseAfterSettings(); + + info("Searching test engine in settings"); + let settings = await promiseSettingsData(); + let found = false; + for (let engine of settings.engines) { + if (engine._name == "Test search engine") { + found = true; + break; + } + } + Assert.ok(found); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_settings_obsolete.js b/toolkit/components/search/tests/xpcshell/test_settings_obsolete.js new file mode 100644 index 0000000000..85022ac931 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_settings_obsolete.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test removing obsolete engine types on upgrade of settings. + */ + +"use strict"; + +async function loadSettingsFile(settingsFile, name) { + let settings = await readJSONFile(do_get_file(settingsFile)); + + settings.metaData.current = name; + settings.metaData.hash = SearchUtils.getVerificationHash(name); + + await promiseSaveSettingsData(settings); +} + +/** + * Start the search service and confirm the engine properties match the expected values. + * + * @param {string} settingsFile + * The path to the settings file to use. + * @param {string} engineName + * The engine name that should be default and is being removed. + */ +async function checkLoadSettingProperties(settingsFile, engineName) { + await loadSettingsFile(settingsFile, engineName); + + const settingsFileWritten = promiseAfterSettings(); + let ss = new SearchService(); + let result = await ss.init(); + + Assert.ok( + Components.isSuccessCode(result), + "Should have successfully initialized the search service" + ); + + await settingsFileWritten; + + let engines = await ss.getEngines(); + + Assert.deepEqual( + engines.map(e => e.name), + ["engine1", "engine2"], + "Should have only loaded the app-provided engines" + ); + + Assert.equal( + (await Services.search.getDefault()).name, + "engine1", + "Should have used the configured default engine" + ); + + removeSettingsFile(); + ss._removeObservers(); +} + +/** + * Test reading from search.json.mozlz4 + */ +add_setup(async function () { + await SearchTestUtils.useTestEngines("data1"); + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_obsolete_distribution_engine() { + await checkLoadSettingProperties( + "data/search-obsolete-distribution.json", + "Distribution" + ); +}); + +add_task(async function test_obsolete_langpack_engine() { + await checkLoadSettingProperties( + "data/search-obsolete-langpack.json", + "Langpack" + ); +}); + +add_task(async function test_obsolete_app_engine() { + await checkLoadSettingProperties("data/search-obsolete-app.json", "App"); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_settings_persist.js b/toolkit/components/search/tests/xpcshell/test_settings_persist.js new file mode 100644 index 0000000000..bed6771554 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_settings_persist.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const CONFIG_DEFAULT = [ + { + webExtension: { + id: "plainengine@search.mozilla.org", + name: "Plain", + search_url: "https://duckduckgo.com/", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + ], + }, + appliesTo: [{ included: { everywhere: true } }], + }, + { + webExtension: { + id: "special-engine@search.mozilla.org", + name: "Special", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + ], + }, + appliesTo: [{ included: { everywhere: true } }], + }, +]; + +const CONFIG_UPDATED = CONFIG_DEFAULT.filter(r => + r.webExtension.id.startsWith("plainengine") +); + +async function startup() { + let settingsFileWritten = promiseAfterSettings(); + let ss = new SearchService(); + await AddonTestUtils.promiseRestartManager(); + await ss.init(false); + await settingsFileWritten; + return ss; +} + +async function updateConfig(config) { + const settings = await RemoteSettings(SearchUtils.SETTINGS_KEY); + settings.get.restore(); + sinon.stub(settings, "get").returns(config); +} + +async function visibleEngines(ss) { + return (await ss.getVisibleEngines()).map(e => e._name); +} + +add_setup(async function () { + await SearchTestUtils.useTestEngines("test-extensions", null, CONFIG_DEFAULT); + registerCleanupFunction(AddonTestUtils.promiseShutdownManager); + await AddonTestUtils.promiseStartupManager(); + // This is only needed as otherwise events will not be properly notified + // due to https://searchfox.org/mozilla-central/source/toolkit/components/search/SearchUtils.jsm#186 + let settingsFileWritten = promiseAfterSettings(); + await Services.search.init(false); + Services.search.wrappedJSObject._removeObservers(); + await settingsFileWritten; +}); + +add_task(async function () { + let ss = await startup(); + Assert.ok( + (await visibleEngines(ss)).includes("Special"), + "Should have both engines on first startup" + ); + + let settingsFileWritten = promiseAfterSettings(); + let engine = await ss.getEngineByName("Special"); + await ss.removeEngine(engine); + await settingsFileWritten; + + Assert.ok( + !(await visibleEngines(ss)).includes("Special"), + "Special has been remove, only Plain should remain" + ); + + ss._removeObservers(); + updateConfig(CONFIG_UPDATED); + ss = await startup(); + + Assert.ok( + !(await visibleEngines(ss)).includes("Special"), + "Updated to new configuration that doesnt have Special" + ); + + ss._removeObservers(); + updateConfig(CONFIG_DEFAULT); + ss = await startup(); + + Assert.ok( + !(await visibleEngines(ss)).includes("Special"), + "Configuration now includes Special but we should remember its removal" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_sort_orders-no-hints.js b/toolkit/components/search/tests/xpcshell/test_sort_orders-no-hints.js new file mode 100644 index 0000000000..d2a2d7dd93 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_sort_orders-no-hints.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check the correct default engines are picked from the configuration list, + * when we have some with the same orderHint, and some without any. + */ + +"use strict"; + +add_setup(async function () { + await AddonTestUtils.promiseStartupManager(); + + await SearchTestUtils.useTestEngines( + "data", + null, + ( + await readJSONFile(do_get_file("data/engines-no-order-hint.json")) + ).data + ); + + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); +}); + +async function checkOrder(type, expectedOrder) { + // Reset the sorted list. + Services.search.wrappedJSObject._cachedSortedEngines = null; + + const sortedEngines = await Services.search[type](); + Assert.deepEqual( + sortedEngines.map(s => s.name), + expectedOrder, + `Should have the expected engine order from ${type}` + ); +} + +add_task(async function test_engine_sort_with_non_builtins_sort() { + await SearchTestUtils.installSearchExtension({ name: "nonbuiltin1" }); + + // As we've added an engine, the pref will have been set to true, but + // we do really want to test the default sort. + Services.search.wrappedJSObject._settings.setMetaDataAttribute( + "useSavedOrder", + false + ); + + const EXPECTED_ORDER = [ + // Default engine. + "Test search engine", + // Alphabetical order for the two with orderHint = 1000. + "engine-chromeicon", + "engine-rel-searchform-purpose", + // Alphabetical order for the remaining engines without orderHint. + "engine-pref", + "engine-resourceicon", + "Test search engine (Reordered)", + ]; + + // We should still have the same built-in engines listed. + await checkOrder("getAppProvidedEngines", EXPECTED_ORDER); + + const expected = [...EXPECTED_ORDER]; + // This is inserted in alphabetical order for the last three. + expected.splice(expected.length - 1, 0, "nonbuiltin1"); + await checkOrder("getEngines", expected); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_sort_orders.js b/toolkit/components/search/tests/xpcshell/test_sort_orders.js new file mode 100644 index 0000000000..836b47ec1e --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_sort_orders.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check the correct default engines are picked from the configuration list, + * and have the correct orders. + */ + +"use strict"; + +const EXPECTED_ORDER = [ + // Default engines + "Test search engine", + "engine-pref", + // Now the engines in orderHint order. + "engine-resourceicon", + "engine-chromeicon", + "engine-rel-searchform-purpose", + "Test search engine (Reordered)", +]; + +add_setup(async function () { + await AddonTestUtils.promiseStartupManager(); + + await SearchTestUtils.useTestEngines(); + + Services.locale.availableLocales = [ + ...Services.locale.availableLocales, + "gd", + ]; + + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + true + ); +}); + +async function checkOrder(type, expectedOrder) { + // Reset the sorted list. + Services.search.wrappedJSObject._cachedSortedEngines = null; + + const sortedEngines = await Services.search[type](); + Assert.deepEqual( + sortedEngines.map(s => s.name), + expectedOrder, + `Should have the expected engine order from ${type}` + ); +} + +add_task(async function test_engine_sort_only_builtins() { + await checkOrder("getAppProvidedEngines", EXPECTED_ORDER); + await checkOrder("getEngines", EXPECTED_ORDER); +}); + +add_task(async function test_engine_sort_with_non_builtins_sort() { + await SearchTestUtils.installSearchExtension({ name: "nonbuiltin1" }); + + // As we've added an engine, the pref will have been set to true, but + // we do really want to test the default sort. + Services.search.wrappedJSObject._settings.setMetaDataAttribute( + "useSavedOrder", + false + ); + + // We should still have the same built-in engines listed. + await checkOrder("getAppProvidedEngines", EXPECTED_ORDER); + + const expected = [...EXPECTED_ORDER]; + expected.splice(EXPECTED_ORDER.length, 0, "nonbuiltin1"); + await checkOrder("getEngines", expected); +}); + +add_task(async function test_engine_sort_with_locale() { + await promiseSetLocale("gd"); + + const expected = [ + "engine-resourceicon-gd", + "engine-pref", + "engine-rel-searchform-purpose", + "engine-chromeicon", + "Test search engine (Reordered)", + ]; + + await checkOrder("getAppProvidedEngines", expected); + expected.push("nonbuiltin1"); + await checkOrder("getEngines", expected); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_telemetry_event_default.js b/toolkit/components/search/tests/xpcshell/test_telemetry_event_default.js new file mode 100644 index 0000000000..84bd1be9cc --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_telemetry_event_default.js @@ -0,0 +1,517 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests for the default engine telemetry event that can be tested via xpcshell, + * related to changing or selecting a different configuration. + * Other tests are typically in browser mochitests. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", +}); + +const BASE_CONFIG = [ + { + webExtension: { + id: "engine@search.mozilla.org", + name: "Test search engine", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "channel", + condition: "purpose", + purpose: "contextmenu", + value: "rcs", + }, + { + name: "channel", + condition: "purpose", + purpose: "keyword", + value: "fflb", + }, + ], + suggest_url: + "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}", + }, + appliesTo: [{ included: { everywhere: true } }], + default: "yes", + }, +]; +const MAIN_CONFIG = [ + { + webExtension: { + id: "engine@search.mozilla.org", + name: "Test search engine", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "channel", + condition: "purpose", + purpose: "contextmenu", + value: "rcs", + }, + { + name: "channel", + condition: "purpose", + purpose: "keyword", + value: "fflb", + }, + ], + suggest_url: + "https://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}", + }, + appliesTo: [{ included: { everywhere: true } }], + default: "no", + }, + { + webExtension: { + id: "engine-chromeicon@search.mozilla.org", + + name: "engine-chromeicon", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + ], + }, + appliesTo: [{ included: { everywhere: true } }], + default: "yes-if-no-other", + }, + { + webExtension: { + id: "engine-fr@search.mozilla.org", + name: "Test search engine (fr)", + search_url: "https://www.google.fr/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "ie", + value: "iso-8859-1", + }, + { + name: "oe", + value: "iso-8859-1", + }, + ], + }, + appliesTo: [ + { included: { everywhere: true } }, + { + included: { locales: { matches: ["fr"] } }, + excluded: { regions: ["DE"] }, + default: "yes", + }, + ], + default: "no", + }, + { + webExtension: { + id: "engine-pref@search.mozilla.org", + name: "engine-pref", + search_url: "https://www.google.com/search", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "code", + condition: "pref", + pref: "code", + }, + { + name: "test", + condition: "pref", + pref: "test", + }, + ], + }, + appliesTo: [ + { included: { everywhere: true } }, + { included: { regions: ["DE"] }, default: "yes" }, + ], + default: "no", + }, + { + webExtension: { + id: "engine2@search.mozilla.org", + name: "A second test engine", + search_url: "https://duckduckgo.com/?q={searchTerms}", + }, + appliesTo: [ + { included: { everywhere: true } }, + { included: { everywhere: true }, experiment: "test1", default: "yes" }, + ], + default: "no", + }, +]; + +const testSearchEngine = { + id: "engine", + name: "Test search engine", + loadPath: SearchUtils.newSearchConfigEnabled + ? "[app]engine@search.mozilla.org" + : "[addon]engine@search.mozilla.org", + submissionURL: "https://www.google.com/search?q=", +}; +const testChromeIconEngine = { + id: "engine-chromeicon", + name: "engine-chromeicon", + loadPath: SearchUtils.newSearchConfigEnabled + ? "[app]engine-chromeicon@search.mozilla.org" + : "[addon]engine-chromeicon@search.mozilla.org", + + submissionURL: "https://www.google.com/search?q=", +}; +const testFrEngine = { + id: "engine-fr", + name: "Test search engine (fr)", + loadPath: SearchUtils.newSearchConfigEnabled + ? "[app]engine-fr@search.mozilla.org" + : "[addon]engine-fr@search.mozilla.org", + submissionURL: "https://www.google.fr/search?q=&ie=iso-8859-1&oe=iso-8859-1", +}; +const testPrefEngine = { + id: "engine-pref", + name: "engine-pref", + loadPath: SearchUtils.newSearchConfigEnabled + ? "[app]engine-pref@search.mozilla.org" + : "[addon]engine-pref@search.mozilla.org", + submissionURL: "https://www.google.com/search?q=", +}; +const testEngine2 = { + id: "engine2", + name: "A second test engine", + loadPath: SearchUtils.newSearchConfigEnabled + ? "[app]engine2@search.mozilla.org" + : "[addon]engine2@search.mozilla.org", + submissionURL: "https://duckduckgo.com/?q=", +}; + +function clearTelemetry() { + Services.telemetry.clearEvents(); + Services.fog.testResetFOG(); +} + +async function checkTelemetry( + source, + prevEngine, + newEngine, + checkPrivate = false, + additionalEventsExpected = false +) { + // TODO Bug 1876178 - Improve engine change telemetry. + // When we reload engines due to a config change, we update the engines as + // they may have changed, we don't track if any attribute has actually changed + // from previous, and so we send out an update regardless. This is why in + // this test we test for the additional `engine-update` event that's recorded. + // In future, we should be more specific about when to record the event and + // so only one event is captured and not two. + let additionalEvent = [ + { + object: checkPrivate ? "change_private" : "change_default", + value: "engine-update", + extra: { + prev_id: prevEngine?.id ?? "", + new_id: prevEngine?.id ?? "", + new_name: prevEngine?.name ?? "", + new_load_path: prevEngine?.loadPath ?? "", + // Telemetry has a limit of 80 characters. + new_sub_url: prevEngine?.submissionURL.slice(0, 80) ?? "", + }, + }, + ]; + + TelemetryTestUtils.assertEvents( + [ + ...(additionalEventsExpected ? additionalEvent : []), + { + object: checkPrivate ? "change_private" : "change_default", + value: source, + extra: { + prev_id: prevEngine?.id ?? "", + new_id: newEngine?.id ?? "", + new_name: newEngine?.name ?? "", + new_load_path: newEngine?.loadPath ?? "", + // Telemetry has a limit of 80 characters. + new_sub_url: newEngine?.submissionURL.slice(0, 80) ?? "", + }, + }, + ], + { category: "search", method: "engine" } + ); + + let snapshot; + if (checkPrivate) { + snapshot = await Glean.searchEnginePrivate.changed.testGetValue(); + } else { + snapshot = await Glean.searchEngineDefault.changed.testGetValue(); + } + + if (additionalEventsExpected) { + delete snapshot[0].timestamp; + Assert.deepEqual( + snapshot[0], + { + category: checkPrivate + ? "search.engine.private" + : "search.engine.default", + name: "changed", + extra: { + change_source: "engine-update", + previous_engine_id: prevEngine?.id ?? "", + new_engine_id: prevEngine?.id ?? "", + new_display_name: prevEngine?.name ?? "", + new_load_path: prevEngine?.loadPath ?? "", + new_submission_url: prevEngine?.submissionURL ?? "", + }, + }, + "Should have received the correct event details" + ); + snapshot.shift(); + } + + delete snapshot[0].timestamp; + Assert.deepEqual( + snapshot[0], + { + category: checkPrivate + ? "search.engine.private" + : "search.engine.default", + name: "changed", + extra: { + change_source: source, + previous_engine_id: prevEngine?.id ?? "", + new_engine_id: newEngine?.id ?? "", + new_display_name: newEngine?.name ?? "", + new_load_path: newEngine?.loadPath ?? "", + new_submission_url: newEngine?.submissionURL ?? "", + }, + }, + "Should have received the correct event details" + ); +} + +let getVariableStub; + +add_setup(async () => { + Region._setHomeRegion("US", false); + Services.locale.availableLocales = [ + ...Services.locale.availableLocales, + "en", + "fr", + ]; + Services.locale.requestedLocales = ["en"]; + + sinon.spy(NimbusFeatures.searchConfiguration, "onUpdate"); + sinon.stub(NimbusFeatures.searchConfiguration, "ready").resolves(); + getVariableStub = sinon.stub( + NimbusFeatures.searchConfiguration, + "getVariable" + ); + getVariableStub.returns(null); + + SearchTestUtils.useMockIdleService(); + Services.fog.initializeFOG(); + sinon.stub( + Services.search.wrappedJSObject, + "_showRemovalOfSearchEngineNotificationBox" + ); + + await SearchTestUtils.useTestEngines("data", null, BASE_CONFIG); + await AddonTestUtils.promiseStartupManager(); + + await Services.search.init(); +}); + +add_task(async function test_configuration_changes_default() { + clearTelemetry(); + + await SearchTestUtils.updateRemoteSettingsConfig(MAIN_CONFIG); + + await checkTelemetry( + "config", + testSearchEngine, + testChromeIconEngine, + false, + true + ); +}); + +add_task(async function test_experiment_changes_default() { + clearTelemetry(); + + let reloadObserved = + SearchTestUtils.promiseSearchNotification("engines-reloaded"); + getVariableStub.callsFake(name => (name == "experiment" ? "test1" : null)); + NimbusFeatures.searchConfiguration.onUpdate.firstCall.args[0](); + await reloadObserved; + + await checkTelemetry( + "experiment", + testChromeIconEngine, + testEngine2, + false, + true + ); + + // Reset the stub so that we are no longer in an experiment. + getVariableStub.returns(null); +}); + +add_task(async function test_locale_changes_default() { + clearTelemetry(); + + let reloadObserved = + SearchTestUtils.promiseSearchNotification("engines-reloaded"); + Services.locale.requestedLocales = ["fr"]; + await reloadObserved; + + await checkTelemetry("locale", testEngine2, testFrEngine, false, true); +}); + +add_task(async function test_region_changes_default() { + clearTelemetry(); + + let reloadObserved = + SearchTestUtils.promiseSearchNotification("engines-reloaded"); + Region._setHomeRegion("DE", true); + await reloadObserved; + + await checkTelemetry("region", testFrEngine, testPrefEngine, false, true); +}); + +add_task(async function test_user_changes_separate_private_pref() { + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + true + ); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); + + await Services.search.setDefaultPrivate( + Services.search.getEngineByName("engine-chromeicon"), + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + Assert.notEqual( + await Services.search.getDefault(), + await Services.search.getDefaultPrivate(), + "Should have different engines for the pre-condition" + ); + + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + false + ); + + clearTelemetry(); + + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + false + ); + + await checkTelemetry("user_private_split", testChromeIconEngine, null, true); + + getVariableStub.returns(null); +}); + +add_task(async function test_experiment_with_separate_default_notifies() { + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault.ui.enabled", + false + ); + Services.prefs.setBoolPref( + SearchUtils.BROWSER_SEARCH_PREF + "separatePrivateDefault", + true + ); + + clearTelemetry(); + + getVariableStub.callsFake(name => + name == "seperatePrivateDefaultUIEnabled" ? true : null + ); + NimbusFeatures.searchConfiguration.onUpdate.firstCall.args[0](); + + await checkTelemetry("experiment", null, testChromeIconEngine, true); + + clearTelemetry(); + + // Reset the stub so that we are no longer in an experiment. + getVariableStub.returns(null); + NimbusFeatures.searchConfiguration.onUpdate.firstCall.args[0](); + + await checkTelemetry("experiment", testChromeIconEngine, null, true); +}); + +add_task(async function test_default_engine_update() { + clearTelemetry(); + let extension = await SearchTestUtils.installSearchExtension( + { + name: "engine", + id: "engine@tests.mozilla.org", + search_url_get_params: `q={searchTerms}&version=1.0`, + search_url: "https://www.google.com/search", + version: "1.0", + }, + { skipUnload: true } + ); + let engine = Services.search.getEngineByName("engine"); + + Assert.ok(!!engine, "Should have loaded the engine"); + + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + clearTelemetry(); + + let promiseChanged = TestUtils.topicObserved( + "browser-search-engine-modified", + (eng, verb) => verb == "engine-changed" + ); + let manifest = SearchTestUtils.createEngineManifest({ + name: "Bar", + id: "engine@tests.mozilla.org", + search_url_get_params: `q={searchTerms}&version=2.0`, + search_url: "https://www.google.com/search", + version: "2.0", + }); + + await extension.upgrade({ + useAddonManager: "permanent", + manifest, + }); + await AddonTestUtils.waitForSearchProviderStartup(extension); + await promiseChanged; + + const defaultEngineData = { + id: engine.telemetryId, + name: "Bar", + loadPath: engine.wrappedJSObject._loadPath, + submissionURL: "https://www.google.com/search?q=&version=2.0", + }; + await checkTelemetry("engine-update", defaultEngineData, defaultEngineData); + await extension.unload(); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_userEngine.js b/toolkit/components/search/tests/xpcshell/test_userEngine.js new file mode 100644 index 0000000000..7d7fa18c0a --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_userEngine.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that User Engines can be installed correctly. + */ + +"use strict"; + +add_setup(async function () { + Services.fog.initializeFOG(); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +}); + +add_task(async function test_user_engine() { + let promiseEngineAdded = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.ADDED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + await Services.search.addUserEngine( + "user", + "https://example.com/user?q={searchTerms}", + "u" + ); + await promiseEngineAdded; + + let engine = Services.search.getEngineByName("user"); + Assert.ok(engine, "Should have installed the engine."); + + Assert.equal(engine.name, "user", "Should have the correct name"); + Assert.equal(engine.description, null, "Should not have a description"); + Assert.deepEqual(engine.aliases, ["u"], "Should have the correct alias"); + + let submission = engine.getSubmission("foo"); + Assert.equal( + submission.uri.spec, + "https://example.com/user?q=foo", + "Should have the correct search url" + ); + + submission = engine.getSubmission("foo", SearchUtils.URL_TYPE.SUGGEST_JSON); + Assert.equal(submission, null, "Should not have a suggest url"); + + Services.search.defaultEngine = engine; + + await assertGleanDefaultEngine({ + normal: { + engineId: "other-user", + displayName: "user", + loadPath: "[user]", + submissionUrl: "blank:", + verified: "verified", + }, + }); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_validate_engines.js b/toolkit/components/search/tests/xpcshell/test_validate_engines.js new file mode 100644 index 0000000000..8a25216057 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_validate_engines.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Ensure all the engines defined in the configuration are valid by +// creating a refined configuration that includes all the engines everywhere. + +"use strict"; + +const { SearchService } = ChromeUtils.importESModule( + "resource://gre/modules/SearchService.sys.mjs" +); + +const ss = new SearchService(); + +add_task(async function test_validate_engines() { + let settings = RemoteSettings(SearchUtils.SETTINGS_KEY); + let config = await settings.get(); + config = config.map(e => { + return { + appliesTo: [ + { + included: { + everywhere: true, + }, + }, + ], + webExtension: e.webExtension, + }; + }); + + sinon.stub(settings, "get").returns(config); + await AddonTestUtils.promiseStartupManager(); + await ss.init(); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_validate_manifests.js b/toolkit/components/search/tests/xpcshell/test_validate_manifests.js new file mode 100644 index 0000000000..8a267296c8 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_validate_manifests.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ExtensionData } = ChromeUtils.importESModule( + "resource://gre/modules/Extension.sys.mjs" +); + +const SEARCH_EXTENSIONS_PATH = "resource://search-extensions"; + +function getFileURI(resourceURI) { + let resHandler = Services.io + .getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + let filePath = resHandler.resolveURI(Services.io.newURI(resourceURI)); + return Services.io.newURI(filePath); +} + +async function getSearchExtensions() { + // Fetching the root will give us the directory listing which we can parse + // for each file name + let list = await fetch(`${SEARCH_EXTENSIONS_PATH}/`).then(req => req.text()); + return list + .split("\n") + .slice(2) + .reduce((acc, line) => { + let parts = line.split(" "); + if (parts.length > 2 && !parts[1].endsWith(".json")) { + // When the directory listing comes from omni jar each engine + // has a trailing slash (engine/) which we dont get locally, or want. + acc.push(parts[1].split("/")[0]); + } + return acc; + }, []); +} + +add_task(async function test_validate_manifest() { + let searchExtensions = await getSearchExtensions(); + ok( + !!searchExtensions.length, + `Found ${searchExtensions.length} search extensions` + ); + for (const xpi of searchExtensions) { + info(`loading: ${SEARCH_EXTENSIONS_PATH}/${xpi}/`); + let fileURI = getFileURI(`${SEARCH_EXTENSIONS_PATH}/${xpi}/`); + let extension = new ExtensionData(fileURI, false); + await extension.loadManifest(); + let locales = await extension.promiseLocales(); + for (let locale of locales.keys()) { + try { + let manifest = await extension.getLocalizedManifest(locale); + ok(!!manifest, `parsed manifest ${xpi.leafName} in ${locale}`); + } catch (e) { + ok( + false, + `FAIL manifest for ${xpi.leafName} in locale ${locale} failed ${e} :: ${e.stack}` + ); + } + } + } +}); diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_builtin_upgrade.js b/toolkit/components/search/tests/xpcshell/test_webextensions_builtin_upgrade.js new file mode 100644 index 0000000000..763338dba6 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_webextensions_builtin_upgrade.js @@ -0,0 +1,264 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Enable SCOPE_APPLICATION for builtin testing. Default in tests is only SCOPE_PROFILE. +// AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION == 5; +Services.prefs.setIntPref("extensions.enabledScopes", 5); + +const { promiseShutdownManager, promiseStartupManager } = AddonTestUtils; + +const TEST_CONFIG = [ + { + webExtension: { + id: "multilocale@search.mozilla.org", + locales: ["af", "an"], + }, + appliesTo: [{ included: { everywhere: true } }], + }, + { + webExtension: { + id: "plainengine@search.mozilla.org", + }, + appliesTo: [{ included: { everywhere: true } }], + params: { + searchUrlGetParams: [ + { + name: "config", + value: "applied", + }, + ], + }, + }, +]; + +async function getEngineNames() { + let engines = await Services.search.getEngines(); + return engines.map(engine => engine._name); +} + +function makePlainExtension(version, name = "Plain") { + return { + useAddonManager: "permanent", + manifest: { + name, + version, + browser_specific_settings: { + gecko: { + id: "plainengine@search.mozilla.org", + }, + }, + chrome_settings_overrides: { + search_provider: { + name, + search_url: "https://duckduckgo.com/", + params: [ + { + name: "q", + value: "{searchTerms}", + }, + { + name: "t", + condition: "purpose", + purpose: "contextmenu", + value: "ffcm", + }, + { + name: "t", + condition: "purpose", + purpose: "keyword", + value: "ffab", + }, + { + name: "t", + condition: "purpose", + purpose: "searchbar", + value: "ffsb", + }, + { + name: "t", + condition: "purpose", + purpose: "homepage", + value: "ffhp", + }, + { + name: "t", + condition: "purpose", + purpose: "newtab", + value: "ffnt", + }, + ], + suggest_url: "https://ac.duckduckgo.com/ac/q={searchTerms}&type=list", + }, + }, + }, + }; +} + +function makeMultiLocaleExtension(version) { + return { + useAddonManager: "permanent", + manifest: { + name: "__MSG_searchName__", + version, + browser_specific_settings: { + gecko: { + id: "multilocale@search.mozilla.org", + }, + }, + default_locale: "an", + chrome_settings_overrides: { + search_provider: { + name: "__MSG_searchName__", + search_url: "__MSG_searchUrl__", + }, + }, + }, + files: { + "_locales/af/messages.json": { + searchUrl: { + message: `https://example.af/?q={searchTerms}&version=${version}`, + description: "foo", + }, + searchName: { + message: `Multilocale AF`, + description: "foo", + }, + }, + "_locales/an/messages.json": { + searchUrl: { + message: `https://example.an/?q={searchTerms}&version=${version}`, + description: "foo", + }, + searchName: { + message: `Multilocale AN`, + description: "foo", + }, + }, + }, + }; +} + +add_setup( + { skip_if: () => SearchUtils.newSearchConfigEnabled }, + async function () { + await SearchTestUtils.useTestEngines("test-extensions", null, TEST_CONFIG); + await promiseStartupManager(); + + registerCleanupFunction(promiseShutdownManager); + await Services.search.init(); + } +); + +add_task( + { skip_if: () => SearchUtils.newSearchConfigEnabled }, + async function basic_multilocale_test() { + Assert.deepEqual(await getEngineNames(), [ + "Multilocale AF", + "Multilocale AN", + "Plain", + ]); + + let ext = ExtensionTestUtils.loadExtension(makeMultiLocaleExtension("2.0")); + await ext.startup(); + await AddonTestUtils.waitForSearchProviderStartup(ext); + + Assert.deepEqual(await getEngineNames(), [ + "Multilocale AF", + "Multilocale AN", + "Plain", + ]); + + let engine = await Services.search.getEngineByName("Multilocale AF"); + Assert.equal( + engine.getSubmission("test").uri.spec, + "https://example.af/?q=test&version=2.0", + "Engine got update" + ); + engine = await Services.search.getEngineByName("Multilocale AN"); + Assert.equal( + engine.getSubmission("test").uri.spec, + "https://example.an/?q=test&version=2.0", + "Engine got update" + ); + + await ext.unload(); + } +); + +add_task( + { skip_if: () => SearchUtils.newSearchConfigEnabled }, + async function upgrade_with_configuration_change_test() { + Assert.deepEqual(await getEngineNames(), [ + "Multilocale AF", + "Multilocale AN", + "Plain", + ]); + + let engine = await Services.search.getEngineByName("Plain"); + Assert.ok(engine.isAppProvided); + Assert.equal( + engine.getSubmission("test").uri.spec, + // This test engine specifies the q and t params in its search_url, therefore + // we get both those and the extra parameter specified in the test config. + "https://duckduckgo.com/?q=test&t=ffsb&config=applied", + "Should have the configuration applied before update." + ); + + let ext = ExtensionTestUtils.loadExtension(makePlainExtension("2.0")); + await ext.startup(); + await AddonTestUtils.waitForSearchProviderStartup(ext); + + Assert.deepEqual(await getEngineNames(), [ + "Multilocale AF", + "Multilocale AN", + "Plain", + ]); + + engine = await Services.search.getEngineByName("Plain"); + Assert.equal( + engine.getSubmission("test").uri.spec, + // This test engine specifies the q and t params in its search_url, therefore + // we get both those and the extra parameter specified in the test config. + "https://duckduckgo.com/?q=test&t=ffsb&config=applied", + "Should still have the configuration applied after update." + ); + + await ext.unload(); + } +); + +add_task( + { skip_if: () => SearchUtils.newSearchConfigEnabled }, + async function test_upgrade_with_name_change() { + Assert.deepEqual(await getEngineNames(), [ + "Multilocale AF", + "Multilocale AN", + "Plain", + ]); + + let ext = ExtensionTestUtils.loadExtension( + makePlainExtension("2.0", "Plain2") + ); + await ext.startup(); + await AddonTestUtils.waitForSearchProviderStartup(ext); + + Assert.deepEqual(await getEngineNames(), [ + "Multilocale AF", + "Multilocale AN", + "Plain2", + ]); + + let engine = await Services.search.getEngineByName("Plain2"); + Assert.equal( + engine.getSubmission("test").uri.spec, + // This test engine specifies the q and t params in its search_url, therefore + // we get both those and the extra parameter specified in the test config. + "https://duckduckgo.com/?q=test&t=ffsb&config=applied", + "Should still have the configuration applied after update." + ); + + await ext.unload(); + } +); diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_install.js b/toolkit/components/search/tests/xpcshell/test_webextensions_install.js new file mode 100644 index 0000000000..6183d6f95a --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_webextensions_install.js @@ -0,0 +1,221 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { promiseShutdownManager, promiseStartupManager } = AddonTestUtils; + +let gBaseUrl; + +async function getEngineNames() { + let engines = await Services.search.getEngines(); + return engines.map(engine => engine._name); +} + +add_setup(async function () { + let server = useHttpServer(); + server.registerContentType("sjs", "sjs"); + gBaseUrl = `http://localhost:${server.identity.primaryPort}/`; + + await SearchTestUtils.useTestEngines("test-extensions"); + await promiseStartupManager(); + + Services.locale.availableLocales = [ + ...Services.locale.availableLocales, + "af", + ]; + + registerCleanupFunction(async () => { + await promiseShutdownManager(); + Services.prefs.clearUserPref("browser.search.region"); + }); +}); + +add_task(async function basic_install_test() { + await Services.search.init(); + await promiseAfterSettings(); + + // On first boot, we get the configuration defaults + Assert.deepEqual(await getEngineNames(), ["Plain", "Special"]); + + // User installs a new search engine + let extension = await SearchTestUtils.installSearchExtension( + { + encoding: "windows-1252", + }, + { skipUnload: true } + ); + Assert.deepEqual((await getEngineNames()).sort(), [ + "Example", + "Plain", + "Special", + ]); + + let engine = await Services.search.getEngineByName("Example"); + Assert.equal( + engine.wrappedJSObject.queryCharset, + "windows-1252", + "Should have the correct charset" + ); + + // User uninstalls their engine + await extension.awaitStartup(); + await extension.unload(); + await promiseAfterSettings(); + Assert.deepEqual(await getEngineNames(), ["Plain", "Special"]); +}); + +add_task(async function test_install_duplicate_engine() { + let name = "Plain"; + consoleAllowList.push(`An engine called ${name} already exists`); + let extension = await SearchTestUtils.installSearchExtension( + { + name, + search_url: "https://example.com/plain", + }, + { skipUnload: true } + ); + + let engine = await Services.search.getEngineByName("Plain"); + let submission = engine.getSubmission("foo"); + Assert.equal( + submission.uri.spec, + "https://duckduckgo.com/?q=foo&t=ffsb", + "Should have not changed the app provided engine." + ); + + // User uninstalls their engine + await extension.unload(); +}); + +add_task( + // Not needed for new configuration. + { skip_if: () => SearchUtils.newSearchConfigEnabled }, + async function basic_multilocale_test() { + await promiseSetHomeRegion("an"); + + Assert.deepEqual(await getEngineNames(), [ + "Plain", + "Special", + "Multilocale AN", + ]); + } +); + +add_task( + // Not needed for new configuration. + { skip_if: () => SearchUtils.newSearchConfigEnabled }, + async function complex_multilocale_test() { + await promiseSetHomeRegion("af"); + + Assert.deepEqual(await getEngineNames(), [ + "Plain", + "Special", + "Multilocale AF", + "Multilocale AN", + ]); + } +); + +add_task( + // Not needed for new configuration. + { skip_if: () => SearchUtils.newSearchConfigEnabled }, + async function test_manifest_selection() { + // Sets the home region without updating. + Region._setHomeRegion("an", false); + await promiseSetLocale("af"); + + let engine = await Services.search.getEngineByName("Multilocale AN"); + Assert.ok( + engine.getIconURL().endsWith("favicon-an.ico"), + "Should have the correct favicon for an extension of one locale using a different locale." + ); + Assert.equal( + engine.description, + "A enciclopedia Libre", + "Should have the correct engine name for an extension of one locale using a different locale." + ); + } +); + +add_task(async function test_load_favicon_invalid() { + let observed = TestUtils.consoleMessageObserved(msg => { + return msg.wrappedJSObject.arguments[0].includes( + "Content type does not match expected" + ); + }); + + // User installs a new search engine + let extension = await SearchTestUtils.installSearchExtension( + { + favicon_url: `${gBaseUrl}/head_search.js`, + }, + { skipUnload: true } + ); + + await observed; + + let engine = await Services.search.getEngineByName("Example"); + Assert.equal(null, engine.getIconURL(), "Should not have set an iconURI"); + + // User uninstalls their engine + await extension.awaitStartup(); + await extension.unload(); + await promiseAfterSettings(); +}); + +add_task(async function test_load_favicon_invalid_redirect() { + let observed = TestUtils.consoleMessageObserved(msg => { + return msg.wrappedJSObject.arguments[0].includes( + "Content type does not match expected" + ); + }); + + // User installs a new search engine + let extension = await SearchTestUtils.installSearchExtension( + { + favicon_url: `${gDataUrl}/iconsRedirect.sjs?type=invalid`, + }, + { skipUnload: true } + ); + + await observed; + + let engine = await Services.search.getEngineByName("Example"); + Assert.equal(null, engine.getIconURL(), "Should not have set an iconURI"); + + // User uninstalls their engine + await extension.awaitStartup(); + await extension.unload(); + await promiseAfterSettings(); +}); + +add_task(async function test_load_favicon_redirect() { + let promiseEngineChanged = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + + // User installs a new search engine + let extension = await SearchTestUtils.installSearchExtension( + { + favicon_url: `${gDataUrl}/iconsRedirect.sjs`, + }, + { skipUnload: true } + ); + + let engine = await Services.search.getEngineByName("Example"); + + await promiseEngineChanged; + + Assert.ok(engine.getIconURL(), "Should have set an iconURI"); + Assert.ok( + engine.getIconURL().startsWith("data:image/x-icon;base64,"), + "Should have saved the expected content type for the icon" + ); + + // User uninstalls their engine + await extension.awaitStartup(); + await extension.unload(); + await promiseAfterSettings(); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_language_switch.js b/toolkit/components/search/tests/xpcshell/test_webextensions_language_switch.js new file mode 100644 index 0000000000..6a781bba5b --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_webextensions_language_switch.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { promiseShutdownManager, promiseStartupManager } = AddonTestUtils; + +add_setup(async function () { + Services.locale.availableLocales = [ + ...Services.locale.availableLocales, + "en", + "de", + "fr", + ]; + Services.locale.requestedLocales = ["en"]; + + await SearchTestUtils.useTestEngines("data1"); + await promiseStartupManager(); + await Services.search.init(); + await promiseAfterSettings(); + + registerCleanupFunction(promiseShutdownManager); +}); + +add_task(async function test_language_switch_changes_name() { + await SearchTestUtils.installSearchExtension( + { + name: "__MSG_engineName__", + id: "engine@tests.mozilla.org", + search_url_get_params: `q={searchTerms}&version=1.0`, + default_locale: "en", + version: "1.0", + }, + { skipUnload: false }, + { + "_locales/en/messages.json": { + engineName: { + message: "English Name", + description: "The Name", + }, + }, + "_locales/fr/messages.json": { + engineName: { + message: "French Name", + description: "The Name", + }, + }, + } + ); + + let engine = Services.search.getEngineById("engine@tests.mozilla.orgdefault"); + Assert.ok(!!engine, "Should have loaded the engine"); + Assert.equal( + engine.name, + "English Name", + "Should have loaded the English version of the name" + ); + + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + let promiseChanged = TestUtils.topicObserved( + "browser-search-engine-modified", + (eng, verb) => verb == "engine-changed" + ); + + await promiseSetLocale("fr"); + + await promiseChanged; + + engine = Services.search.getEngineById("engine@tests.mozilla.orgdefault"); + Assert.ok(!!engine, "Should still be available"); + Assert.equal( + engine.name, + "French Name", + "Should have updated to the French version of the name" + ); + + Assert.equal( + (await Services.search.getDefault()).id, + engine.id, + "Should have kept the default engine the same" + ); + + promiseChanged = TestUtils.topicObserved( + "browser-search-engine-modified", + (eng, verb) => verb == "engine-changed" + ); + + // Check for changing to a locale the add-on doesn't have. + await promiseSetLocale("de"); + + await promiseChanged; + + engine = Services.search.getEngineById("engine@tests.mozilla.orgdefault"); + Assert.ok(!!engine, "Should still be available"); + Assert.equal( + engine.name, + "English Name", + "Should have fallen back to the default locale (English) version of the name" + ); + + Assert.equal( + (await Services.search.getDefault()).id, + engine.id, + "Should have kept the default engine the same" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_migrate_to.js b/toolkit/components/search/tests/xpcshell/test_webextensions_migrate_to.js new file mode 100644 index 0000000000..cb44ebde84 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_webextensions_migrate_to.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test migrating legacy add-on engines in background. + */ + +"use strict"; + +add_setup(async function () { + useHttpServer("opensearch"); + await AddonTestUtils.promiseStartupManager(); + await SearchTestUtils.useTestEngines("data1"); + + let data = await readJSONFile(do_get_file("data/search-migration.json")); + + await promiseSaveSettingsData(data); + + await Services.search.init(); + + // We need the extension installed for this test, but we do not want to + // trigger the functions that happen on installation, so stub that out. + // The manifest already has details of this engine. + let oldFunc = Services.search.wrappedJSObject.addEnginesFromExtension; + Services.search.wrappedJSObject.addEnginesFromExtension = () => {}; + + // Add the add-on so add-on manager has a valid item. + await SearchTestUtils.installSearchExtension({ + id: "simple", + name: "simple search", + search_url: "https://example.com/", + }); + + Services.search.wrappedJSObject.addEnginesFromExtension = oldFunc; +}); + +add_task(async function test_migrateLegacyEngineDifferentName() { + await Services.search.init(); + + let engine = Services.search.getEngineByName("simple"); + Assert.ok(engine, "Should have the legacy add-on engine."); + + // Set this engine as default, the new engine should become the default + // after migration. + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + engine = Services.search.getEngineByName("simple search"); + Assert.ok(engine, "Should have the WebExtension engine."); + + await Services.search.runBackgroundChecks(); + + engine = Services.search.getEngineByName("simple"); + Assert.ok(!engine, "Should have removed the legacy add-on engine"); + + engine = Services.search.getEngineByName("simple search"); + Assert.ok(engine, "Should have kept the WebExtension engine."); + + Assert.equal( + (await Services.search.getDefault()).name, + engine.name, + "Should have switched to the WebExtension engine as default." + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_normandy_upgrade.js b/toolkit/components/search/tests/xpcshell/test_webextensions_normandy_upgrade.js new file mode 100644 index 0000000000..4beb614510 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_webextensions_normandy_upgrade.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +SearchTestUtils.initXPCShellAddonManager(this, "system"); + +async function restart() { + Services.search.wrappedJSObject.reset(); + await AddonTestUtils.promiseRestartManager(); + await Services.search.init(false); +} + +const CONFIG_DEFAULT = [ + { + webExtension: { id: "plainengine@search.mozilla.org" }, + appliesTo: [{ included: { everywhere: true } }], + }, +]; + +const CONFIG_UPDATED = [ + { + webExtension: { id: "plainengine@search.mozilla.org" }, + appliesTo: [{ included: { everywhere: true } }], + }, + { + webExtension: { id: "example@search.mozilla.org" }, + appliesTo: [{ included: { everywhere: true } }], + }, +]; + +async function getEngineNames() { + let engines = await Services.search.getAppProvidedEngines(); + return engines.map(engine => engine._name); +} + +add_setup( + { skip_if: () => SearchUtils.newSearchConfigEnabled }, + async function () { + await SearchTestUtils.useTestEngines( + "test-extensions", + null, + CONFIG_DEFAULT + ); + await AddonTestUtils.promiseStartupManager(); + registerCleanupFunction(AddonTestUtils.promiseShutdownManager); + SearchTestUtils.useMockIdleService(); + await Services.search.init(); + } +); + +// Test the situation where we receive an updated configuration +// that references an engine that doesnt exist locally as it +// will be installed by Normandy. +add_task( + { skip_if: () => SearchUtils.newSearchConfigEnabled }, + async function test_config_before_normandy() { + // Ensure initial default setup. + await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_DEFAULT); + await restart(); + Assert.deepEqual(await getEngineNames(), ["Plain"]); + // Updated configuration references nonexistant engine. + await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_UPDATED); + Assert.deepEqual( + await getEngineNames(), + ["Plain"], + "Updated engine hasnt been installed yet" + ); + // Normandy then installs the engine. + let addon = await SearchTestUtils.installSystemSearchExtension(); + Assert.deepEqual( + await getEngineNames(), + ["Plain", "Example"], + "Both engines are now enabled" + ); + await addon.unload(); + } +); + +// Test the situation where we receive a newly installed +// engine from Normandy followed by the update to the +// configuration that uses that engine. +add_task( + { skip_if: () => SearchUtils.newSearchConfigEnabled }, + async function test_normandy_before_config() { + // Ensure initial default setup. + await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_DEFAULT); + await restart(); + Assert.deepEqual(await getEngineNames(), ["Plain"]); + // Normandy installs the enigne. + let addon = await SearchTestUtils.installSystemSearchExtension(); + Assert.deepEqual( + await getEngineNames(), + ["Plain"], + "Normandy engine ignored as not in config yet" + ); + // Configuration is updated to use the engine. + await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_UPDATED); + Assert.deepEqual( + await getEngineNames(), + ["Plain", "Example"], + "Both engines are now enabled" + ); + await addon.unload(); + } +); diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_startup_duplicate.js b/toolkit/components/search/tests/xpcshell/test_webextensions_startup_duplicate.js new file mode 100644 index 0000000000..3e42fdee13 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_webextensions_startup_duplicate.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const lazy = {}; + +const { promiseShutdownManager, promiseStartupManager } = AddonTestUtils; + +ChromeUtils.defineESModuleGetters(lazy, { + ExtensionTestUtils: + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs", +}); + +add_setup(async function () { + let server = useHttpServer(); + server.registerContentType("sjs", "sjs"); + await SearchTestUtils.useTestEngines("test-extensions"); + await promiseStartupManager(); + + registerCleanupFunction(async () => { + await promiseShutdownManager(); + }); +}); + +add_task(async function test_install_duplicate_engine_startup() { + let name = "Plain"; + let id = "plain@tests.mozilla.org"; + consoleAllowList.push( + `#installExtensionEngine failed for ${id}`, + `An engine called ${name} already exists` + ); + // Do not use SearchTestUtils.installSearchExtension, as we need to manually + // start the search service after installing the extension. + let extensionInfo = { + useAddonManager: "permanent", + files: {}, + manifest: SearchTestUtils.createEngineManifest({ + name, + search_url: "https://example.com/plain", + }), + }; + + let extension = lazy.ExtensionTestUtils.loadExtension(extensionInfo); + await extension.startup(); + + await Services.search.init(); + + await AddonTestUtils.waitForSearchProviderStartup(extension); + let engine = await Services.search.getEngineByName(name); + let submission = engine.getSubmission("foo"); + Assert.equal( + submission.uri.spec, + "https://duckduckgo.com/?q=foo&t=ffsb", + "Should have not changed the app provided engine." + ); + + await extension.unload(); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_startup_remove.js b/toolkit/components/search/tests/xpcshell/test_webextensions_startup_remove.js new file mode 100644 index 0000000000..a357320c7b --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_webextensions_startup_remove.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ENGINE_ID = "enginetest@example.com"; +let xpi; +let profile = do_get_profile().clone(); + +add_setup(async function () { + await SearchTestUtils.useTestEngines("data1"); + xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version: "1.0", + browser_specific_settings: { + gecko: { id: ENGINE_ID }, + }, + chrome_settings_overrides: { + search_provider: { + name: "Test Engine", + search_url: `https://example.com/?q={searchTerms}`, + }, + }, + }, + }); + await AddonTestUtils.manuallyInstall(xpi); +}); + +add_task(async function test_removeAddonOnStartup() { + // First startup the add-on manager and ensure the engine is installed. + await AddonTestUtils.promiseStartupManager(); + let promise = promiseAfterSettings(); + await Services.search.init(); + + let engine = Services.search.getEngineByName("Test Engine"); + let allEngines = await Services.search.getEngines(); + + Assert.ok(!!engine, "Should have installed the test engine"); + + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await promise; + + await AddonTestUtils.promiseShutdownManager(); + + // Now remove it, reset the search service and start up the add-on manager. + // Note: the saved settings will have the engine in. If this didn't work, + // the engine would still be present. + await IOUtils.remove( + PathUtils.join(profile.path, "extensions", `${ENGINE_ID}.xpi`) + ); + + let removePromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.REMOVED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + Services.search.wrappedJSObject.reset(); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); + await removePromise; + + Assert.ok( + !Services.search.getEngineByName("Test Engine"), + "Should have removed the test engine" + ); + + let newEngines = await Services.search.getEngines(); + Assert.deepEqual( + newEngines.map(e => e.name), + allEngines.map(e => e.name).filter(n => n != "Test Engine"), + "Should no longer have the test engine in the full list" + ); + let newDefault = await Services.search.getDefault(); + Assert.equal( + newDefault.name, + "engine1", + "Should have changed the default engine back to the configuration default" + ); + + await AddonTestUtils.promiseShutdownManager(); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_upgrade.js b/toolkit/components/search/tests/xpcshell/test_webextensions_upgrade.js new file mode 100644 index 0000000000..bfd789780a --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_webextensions_upgrade.js @@ -0,0 +1,188 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { promiseShutdownManager, promiseStartupManager } = AddonTestUtils; + +add_setup(async function () { + await SearchTestUtils.useTestEngines("data1"); + await promiseStartupManager(); + await Services.search.init(); + await promiseAfterSettings(); + + registerCleanupFunction(promiseShutdownManager); +}); + +add_task(async function test_basic_upgrade() { + let extension = await SearchTestUtils.installSearchExtension( + { + version: "1.0", + search_url_get_params: `q={searchTerms}&version=1.0`, + keyword: "foo", + }, + { skipUnload: true } + ); + + let engine = await Services.search.getEngineByAlias("foo"); + Assert.ok(engine, "Can fetch engine with alias"); + engine.alias = "testing"; + + engine = await Services.search.getEngineByAlias("testing"); + Assert.ok(engine, "Can fetch engine by alias"); + let params = engine.getSubmission("test").uri.query.split("&"); + Assert.ok(params.includes("version=1.0"), "Correct version installed"); + + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + let promiseChanged = TestUtils.topicObserved( + "browser-search-engine-modified", + (eng, verb) => verb == "engine-changed" + ); + + let manifest = SearchTestUtils.createEngineManifest({ + version: "2.0", + search_url_get_params: `q={searchTerms}&version=2.0`, + keyword: "bar", + }); + await extension.upgrade({ + useAddonManager: "permanent", + manifest, + }); + await AddonTestUtils.waitForSearchProviderStartup(extension); + await promiseChanged; + + engine = await Services.search.getEngineByAlias("testing"); + Assert.ok(engine, "Engine still has alias set"); + + params = engine.getSubmission("test").uri.query.split("&"); + Assert.ok(params.includes("version=2.0"), "Correct version installed"); + + Assert.equal( + Services.search.defaultEngine.name, + "Example", + "Should have retained the same default engine" + ); + + await extension.unload(); + await promiseAfterSettings(); +}); + +add_task(async function test_upgrade_changes_name() { + let extension = await SearchTestUtils.installSearchExtension( + { + name: "engine", + id: "engine@tests.mozilla.org", + search_url_get_params: `q={searchTerms}&version=1.0`, + version: "1.0", + }, + { skipUnload: true } + ); + + let engine = Services.search.getEngineByName("engine"); + Assert.ok(!!engine, "Should have loaded the engine"); + + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + // When we add engines currently, we normally force using the saved order. + // Reset that here, so we can check the order is reset in the case this + // is a application provided engine change. + Services.search.wrappedJSObject._settings.setMetaDataAttribute( + "useSavedOrder", + false + ); + Services.search.getEngineByName("engine1").wrappedJSObject._orderHint = null; + Services.search.getEngineByName("engine2").wrappedJSObject._orderHint = null; + + Assert.deepEqual( + (await Services.search.getVisibleEngines()).map(e => e.name), + ["engine1", "engine2", "engine"], + "Should have the expected order initially" + ); + + let promiseChanged = TestUtils.topicObserved( + "browser-search-engine-modified", + (eng, verb) => verb == "engine-changed" + ); + + let manifest = SearchTestUtils.createEngineManifest({ + name: "Bar", + id: "engine@tests.mozilla.org", + search_url_get_params: `q={searchTerms}&version=2.0`, + version: "2.0", + }); + await extension.upgrade({ + useAddonManager: "permanent", + manifest, + }); + await AddonTestUtils.waitForSearchProviderStartup(extension); + + await promiseChanged; + + engine = Services.search.getEngineByName("Bar"); + Assert.ok(!!engine, "Should be able to get the new engine"); + + Assert.equal( + (await Services.search.getDefault()).name, + "Bar", + "Should have kept the default engine the same" + ); + + Assert.deepEqual( + (await Services.search.getVisibleEngines()).map(e => e.name), + // Expected order: Default, then others in alphabetical. + ["engine1", "Bar", "engine2"], + "Should have updated the engine order" + ); + + await extension.unload(); + await promiseAfterSettings(); +}); + +add_task(async function test_upgrade_to_existing_name_not_allowed() { + let extension = await SearchTestUtils.installSearchExtension( + { + name: "engine", + search_url_get_params: `q={searchTerms}&version=1.0`, + version: "1.0", + }, + { skipUnload: true } + ); + + let engine = Services.search.getEngineByName("engine"); + Assert.ok(!!engine, "Should have loaded the engine"); + + let promise = AddonTestUtils.waitForSearchProviderStartup(extension); + let name = "engine1"; + consoleAllowList.push(`An engine called ${name} already exists`); + let manifest = SearchTestUtils.createEngineManifest({ + name, + search_url_get_params: `q={searchTerms}&version=2.0`, + version: "2.0", + }); + await extension.upgrade({ + useAddonManager: "permanent", + manifest, + }); + await promise; + + Assert.equal( + Services.search.getEngineByName("engine1").getSubmission("").uri.spec, + "https://1.example.com/", + "Should have not changed the original engine" + ); + + console.log((await Services.search.getEngines()).map(e => e.name)); + + engine = Services.search.getEngineByName("engine"); + Assert.ok(!!engine, "Should still be able to get the engine by the old name"); + + await extension.unload(); + await promiseAfterSettings(); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_webextensions_valid.js b/toolkit/components/search/tests/xpcshell/test_webextensions_valid.js new file mode 100644 index 0000000000..36ba8155bf --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_webextensions_valid.js @@ -0,0 +1,199 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const { promiseShutdownManager, promiseStartupManager } = AddonTestUtils; + +let extension; +let extensionPostData; +let oldRemoveEngineFunc; + +add_setup(async function () { + await SearchTestUtils.useTestEngines("simple-engines"); + await promiseStartupManager(); + + Services.telemetry.canRecordExtended = true; + + await Services.search.init(); + await promiseAfterSettings(); + + extension = await SearchTestUtils.installSearchExtension( + {}, + { skipUnload: true } + ); + extensionPostData = await SearchTestUtils.installSearchExtension( + { + name: "PostData", + search_url_post_params: "?q={searchTerms}&post=1", + }, + { skipUnload: true } + ); + await extension.awaitStartup(); + await extensionPostData.awaitStartup(); + + // For these tests, stub-out the removeEngine function, so that when we + // remove it from the add-on manager, the engine is left in the search + // settings. + oldRemoveEngineFunc = Services.search.wrappedJSObject.removeEngine.bind( + Services.search.wrappedJSObject + ); + Services.search.wrappedJSObject.removeEngine = () => {}; + + registerCleanupFunction(async () => { + await promiseShutdownManager(); + }); +}); + +add_task(async function test_valid_extensions_do_nothing() { + Services.telemetry.clearScalars(); + + Assert.ok( + Services.search.getEngineByName("Example"), + "Should have installed the engine" + ); + Assert.ok( + !!Services.search.getEngineByName("PostData"), + "Should have installed the PostData engine" + ); + + await Services.search.runBackgroundChecks(); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + + Assert.deepEqual(scalars, {}, "Should not have recorded any issues"); +}); + +add_task(async function test_different_name() { + Services.telemetry.clearScalars(); + + let engine = Services.search.getEngineByName("Example"); + + engine.wrappedJSObject._name = "Example Test"; + + await Services.search.runBackgroundChecks(); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "browser.searchinit.engine_invalid_webextension", + extension.id, + 5 + ); + + engine.wrappedJSObject._name = "Example"; +}); + +add_task(async function test_different_url() { + Services.telemetry.clearScalars(); + + let engine = Services.search.getEngineByName("Example"); + + engine.wrappedJSObject._urls = []; + engine.wrappedJSObject._setUrls({ + search_url: "https://example.com/123", + search_url_get_params: "?q={searchTerms}", + }); + + await Services.search.runBackgroundChecks(); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "browser.searchinit.engine_invalid_webextension", + extension.id, + 6 + ); +}); + +add_task(async function test_different_url_post_data() { + Services.telemetry.clearScalars(); + + let engine = Services.search.getEngineByName("PostData"); + + engine.wrappedJSObject._urls = []; + engine.wrappedJSObject._setUrls({ + search_url: "https://example.com/123", + search_url_post_params: "?q={searchTerms}", + }); + + await Services.search.runBackgroundChecks(); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "browser.searchinit.engine_invalid_webextension", + extensionPostData.id, + 6 + ); +}); + +add_task(async function test_extension_no_longer_specifies_engine() { + Services.telemetry.clearScalars(); + + let extensionInfo = { + useAddonManager: "permanent", + manifest: { + version: "2.0", + browser_specific_settings: { + gecko: { + id: "example@tests.mozilla.org", + }, + }, + }, + }; + + await extension.upgrade(extensionInfo); + + await Services.search.runBackgroundChecks(); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "browser.searchinit.engine_invalid_webextension", + extension.id, + 4 + ); +}); + +add_task(async function test_disabled_extension() { + // We don't clear scalars between tests to ensure the scalar gets set + // to the new value, rather than added. + + // Disable the extension, this won't remove the search engine because we've + // stubbed removeEngine. + await extension.addon.disable(); + + await Services.search.runBackgroundChecks(); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "browser.searchinit.engine_invalid_webextension", + extension.id, + 2 + ); + + extension.addon.enable(); + await extension.awaitStartup(); +}); + +add_task(async function test_missing_extension() { + // We don't clear scalars between tests to ensure the scalar gets set + // to the new value, rather than added. + + let extensionId = extension.id; + // Remove the extension, this won't remove the search engine because we've + // stubbed removeEngine. + await extension.unload(); + + await Services.search.runBackgroundChecks(); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "browser.searchinit.engine_invalid_webextension", + extensionId, + 1 + ); + + await oldRemoveEngineFunc(Services.search.getEngineByName("Example")); +}); diff --git a/toolkit/components/search/tests/xpcshell/xpcshell.toml b/toolkit/components/search/tests/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..47847c93de --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/xpcshell.toml @@ -0,0 +1,306 @@ +[DEFAULT] +firefox-appdir = "browser" +head = "head_search.js" +dupe-manifest = "" +tags = "searchmain" +skip-if = ["os == 'android'"] +prefs = ["browser.search.removeEngineInfobar.enabled=true"] + +support-files = [ + "data/engine.xml", + "data/engine/manifest.json", + "data/engine2.xml", + "data/engine2/manifest.json", + "data/engine-app/manifest.json", + "data/engine-diff-name/manifest.json", + "data/engine-diff-name/_locales/en/messages.json", + "data/engine-diff-name/_locales/gd/messages.json", + "data/engine-fr.xml", + "data/engine-fr/manifest.json", + "data/engine-reordered/manifest.json", + "data/engineMaker.sjs", + "data/engine-pref/manifest.json", + "data/engine-rel-searchform-purpose/manifest.json", + "data/engine-system-purpose/manifest.json", + "data/engineImages.xml", + "data/engine-chromeicon/manifest.json", + "data/engine-purposes/manifest.json", + "data/engine-resourceicon/manifest.json", + "data/engine-resourceicon/_locales/en/messages.json", + "data/engine-resourceicon/_locales/gd/messages.json", + "data/engine-same-name/manifest.json", + "data/engine-same-name/_locales/en/messages.json", + "data/engine-same-name/_locales/gd/messages.json", + "data/engines-no-order-hint.json", + "data/engines.json", + "data/iconsRedirect.sjs", + "data/search.json", + "data/search-legacy.json", + "data/search-legacy-correct-default-engine-hashes.json", + "data/search-legacy-no-ids.json", + "data/search-legacy-old-loadPaths.json", + "data/search-legacy-wrong-default-engine-hashes.json", + "data/search-legacy-wrong-third-party-engine-hashes.json", + "data/search-obsolete-app.json", + "data/search-obsolete-distribution.json", + "data/search-obsolete-langpack.json", + "data/searchSuggestions.sjs", + "data/geolookup-extensions/multilocale/favicon.ico", + "data/geolookup-extensions/multilocale/manifest.json", + "data/geolookup-extensions/multilocale/_locales/af/messages.json", + "data/geolookup-extensions/multilocale/_locales/an/messages.json", + "data1/engine1/manifest.json", + "data1/engine2/manifest.json", + "data1/exp2/manifest.json", + "data1/exp3/manifest.json", + "data1/engines.json", + "data1/search-config-v2.json", + "simple-engines/engines.json", + "simple-engines/search-config-v2.json", + "simple-engines/basic/manifest.json", + "simple-engines/simple/manifest.json", + "test-extensions/engines.json", + "test-extensions/plainengine/favicon.ico", + "test-extensions/plainengine/manifest.json", + "test-extensions/special-engine/favicon.ico", + "test-extensions/special-engine/manifest.json", + "test-extensions/multilocale/favicon-af.ico", + "test-extensions/multilocale/favicon-an.ico", + "test-extensions/multilocale/manifest.json", + "test-extensions/multilocale/_locales/af/messages.json", + "test-extensions/multilocale/_locales/an/messages.json", +] + +["test_SearchStaticData.js"] + +["test_appDefaultEngine.js"] + +["test_async.js"] + +["test_config_engine_params.js"] +support-files = [ + "method-extensions/get/manifest.json", + "method-extensions/post/manifest.json", + "method-extensions/engines.json", +] + +["test_defaultEngine.js"] + +["test_defaultEngine_experiments.js"] + +["test_defaultEngine_fallback.js"] + +["test_defaultPrivateEngine.js"] + +["test_engine_alias.js"] + +["test_engine_ids.js"] + +["test_engine_multiple_alias.js"] + +["test_engine_old_selector.js"] + +["test_engine_old_selector_application.js"] + +["test_engine_old_selector_application_distribution.js"] + +["test_engine_old_selector_application_name.js"] + +["test_engine_old_selector_order.js"] + +["test_engine_old_selector_override.js"] + +["test_engine_old_selector_remote_settings.js"] +tags = "remotesettings searchmain" + +["test_engine_old_selector_remote_override.js"] + +["test_engine_selector_engine_orders.js"] + +["test_engine_selector_defaults.js"] + +["test_engine_selector_environment.js"] + +["test_engine_selector_variants.js"] + +["test_engine_set_alias.js"] + +["test_getSubmission_encoding.js"] + +["test_getSubmission_params.js"] + +["test_getSubmission_params_pref.js"] + +["test_getSubmission_params_prefNimbus.js"] + +["test_getSubmission_params_prefNimbus_invalid.js"] + +["test_getSubmission_params_purpose.js"] + +["test_identifiers.js"] + +["test_ignorelist.js"] + +["test_ignorelist_update.js"] + +["test_initialization.js"] + +["test_initialization_status_telemetry.js"] + +["test_initialization_with_region.js"] + +["test_list_json_locale.js"] + +["test_list_json_no_private_default.js"] + +["test_list_json_searchdefault.js"] + +["test_list_json_searchorder.js"] + +["test_maybereloadengine_order.js"] + +["test_migrateWebExtensionEngine.js"] + +["test_missing_engine.js"] + +["test_nodb_pluschanges.js"] + +["test_notifications.js"] + +["test_opensearch.js"] +support-files = [ + "opensearch/mozilla-ns.xml", + "opensearch/post.xml", + "opensearch/searchform-invalid.xml", + "opensearch/simple.xml", + "opensearch/suggestion.xml", + "opensearch/suggestion-alternate.xml", +] + +["test_opensearch_icon.js"] +support-files = [ + "data/bigIcon.ico", + "data/remoteIcon.ico", + "data/svgIcon.svg", +] + +["test_opensearch_icons_invalid.js"] +support-files = [ + "opensearch/chromeicon.xml", + "opensearch/resourceicon.xml", +] + +["test_opensearch_install_errors.js"] +support-files = ["opensearch/invalid.xml"] + +["test_opensearch_telemetry.js"] +support-files = [ + "opensearch/secure-and-securely-updated1.xml", + "opensearch/secure-and-securely-updated2.xml", + "opensearch/secure-and-securely-updated3.xml", + "opensearch/secure-and-securely-updated-insecure-form.xml", + "opensearch/secure-and-insecurely-updated1.xml", + "opensearch/secure-and-insecurely-updated2.xml", + "opensearch/insecure-and-securely-updated1.xml", + "opensearch/insecure-and-insecurely-updated1.xml", + "opensearch/insecure-and-insecurely-updated2.xml", + "opensearch/secure-and-no-update-url1.xml", + "opensearch/insecure-and-no-update-url1.xml", + "opensearch/secure-localhost.xml", + "opensearch/secure-onionv2.xml", + "opensearch/secure-onionv3.xml", +] + +["test_opensearch_update.js"] + +["test_override_allowlist.js"] + +["test_override_allowlist_switch.js"] + +["test_parseSubmissionURL.js"] + +["test_policyEngine.js"] + +["test_reload_engines.js"] + +["test_reload_engines_duplicate.js"] + +["test_reload_engines_experiment.js"] + +["test_reload_engines_locales.js"] + +["test_remove_engine_notification_box.js"] + +["test_remove_profile_engine.js"] + +["test_save_sorted_engines.js"] + +["test_searchSuggest.js"] + +["test_searchSuggest_cookies.js"] + +["test_searchSuggest_extraParams.js"] + +["test_searchSuggest_private.js"] + +["test_searchTermFromResult.js"] + +["test_searchUrlDomain.js"] + +["test_selectedEngine.js"] + +["test_sendSubmissionURL.js"] + +["test_settings.js"] + +["test_settings_broken.js"] + +["test_settings_duplicate.js"] + +["test_settings_good.js"] + +["test_settings_ignorelist.js"] +support-files = ["data/search_ignorelist.json"] + +["test_settings_migration_hideOneOffs.js"] + +["test_settings_migration_ids.js"] + +["test_settings_migration_loadPath.js"] + +["test_settings_none.js"] + +["test_settings_obsolete.js"] + +["test_settings_persist.js"] + +["test_sort_orders-no-hints.js"] + +["test_sort_orders.js"] + +["test_telemetry_event_default.js"] + +["test_userEngine.js"] + +["test_validate_engines.js"] + +["test_validate_manifests.js"] + +["test_webextensions_builtin_upgrade.js"] + +["test_webextensions_install.js"] + +["test_webextensions_language_switch.js"] + +["test_webextensions_migrate_to.js"] +support-files = ["data/search-migration.json"] + +["test_webextensions_normandy_upgrade.js"] + +["test_webextensions_startup_duplicate.js"] + +["test_webextensions_startup_remove.js"] + +["test_webextensions_upgrade.js"] + +["test_webextensions_valid.js"] |