diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/extensions/test/mochitest | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/extensions/test/mochitest')
172 files changed, 17758 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/mochitest/.eslintrc.js b/toolkit/components/extensions/test/mochitest/.eslintrc.js new file mode 100644 index 0000000000..a776405c9d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/.eslintrc.js @@ -0,0 +1,12 @@ +"use strict"; + +module.exports = { + env: { + browser: true, + webextensions: true, + }, + + rules: { + "no-shadow": 0, + }, +}; diff --git a/toolkit/components/extensions/test/mochitest/chrome.ini b/toolkit/components/extensions/test/mochitest/chrome.ini new file mode 100644 index 0000000000..209f11b864 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/chrome.ini @@ -0,0 +1,37 @@ +[DEFAULT] +support-files = + chrome_cleanup_script.js + head.js + head_cookies.js + file_image_good.png + file_image_great.png + file_sample.html + file_with_images.html + webrequest_chromeworker.js + webrequest_test.jsm +prefs = + security.mixed_content.upgrade_display_content=false +tags = webextensions in-process-webextensions + +# NO NEW TESTS. mochitest-chrome does not run under e10s, avoid adding new +# tests here unless absolutely necessary. + +[test_chrome_ext_contentscript_data_uri.html] +[test_chrome_ext_contentscript_telemetry.html] +skip-if = (os == 'linux' && bits == 64) #Bug 1393920 +[test_chrome_ext_contentscript_unrecognizedprop_warning.html] +[test_chrome_ext_downloads_open.html] +[test_chrome_ext_downloads_saveAs.html] +skip-if = (verify && !debug && (os == 'win')) || (os == 'android') +[test_chrome_ext_downloads_uniquify.html] +[test_chrome_ext_permissions.html] +skip-if = os == 'android' # Bug 1350559 +[test_chrome_ext_trackingprotection.html] +[test_chrome_ext_webnavigation_resolved_urls.html] +[test_chrome_ext_webrequest_background_events.html] +[test_chrome_ext_webrequest_host_permissions.html] +skip-if = verify +[test_chrome_ext_webrequest_mozextension.html] +skip-if = true # Bug 1404172 +[test_chrome_native_messaging_paths.html] +skip-if = os != "mac" && os != "linux" diff --git a/toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js b/toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js new file mode 100644 index 0000000000..397996b15c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js @@ -0,0 +1,66 @@ +"use strict"; + +/* global addMessageListener, sendAsyncMessage */ + +const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +let listener = msg => { + void (msg instanceof Ci.nsIConsoleMessage); + dump(`Console message: ${msg}\n`); +}; + +Services.console.registerListener(listener); + +let getBrowserApp, getTabBrowser; +if (AppConstants.MOZ_BUILD_APP === "mobile/android") { + getBrowserApp = win => win.BrowserApp; + getTabBrowser = tab => tab.browser; +} else { + getBrowserApp = win => win.gBrowser; + getTabBrowser = tab => tab.linkedBrowser; +} + +function* iterBrowserWindows() { + for (let win of Services.wm.getEnumerator("navigator:browser")) { + if (!win.closed && getBrowserApp(win)) { + yield win; + } + } +} + +let initialTabs = new Map(); +for (let win of iterBrowserWindows()) { + initialTabs.set(win, new Set(getBrowserApp(win).tabs)); +} + +addMessageListener("check-cleanup", extensionId => { + Services.console.unregisterListener(listener); + + let results = { + extraWindows: [], + extraTabs: [], + }; + + for (let win of iterBrowserWindows()) { + if (initialTabs.has(win)) { + let tabs = initialTabs.get(win); + + for (let tab of getBrowserApp(win).tabs) { + if (!tabs.has(tab)) { + results.extraTabs.push(getTabBrowser(tab).currentURI.spec); + } + } + } else { + results.extraWindows.push( + Array.from(win.gBrowser.tabs, tab => getTabBrowser(tab).currentURI.spec) + ); + } + } + + initialTabs = null; + + sendAsyncMessage("cleanup-results", results); +}); diff --git a/toolkit/components/extensions/test/mochitest/chrome_head.js b/toolkit/components/extensions/test/mochitest/chrome_head.js new file mode 100644 index 0000000000..3918c74e44 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/chrome_head.js @@ -0,0 +1 @@ +"use strict"; diff --git a/toolkit/components/extensions/test/mochitest/file_WebNavigation_page1.html b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page1.html new file mode 100644 index 0000000000..663ebc6112 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page1.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> + +<html> +<body> + +<iframe src="file_WebNavigation_page2.html" width="200" height="200"></iframe> + +<form> +</form> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_WebNavigation_page2.html b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page2.html new file mode 100644 index 0000000000..cc1acc83d6 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page2.html @@ -0,0 +1,7 @@ +<!DOCTYPE HTML> + +<html> +<body> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_WebNavigation_page3.html b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page3.html new file mode 100644 index 0000000000..a0a26a2e9d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page3.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> + +<html> +<body> + +<a id="elt" href="file_WebNavigation_page3.html#ref">click me</a> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html b/toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html new file mode 100644 index 0000000000..24c7a42986 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +<script> +"use strict"; +</script> +</head> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_contains_iframe.html b/toolkit/components/extensions/test/mochitest/file_contains_iframe.html new file mode 100644 index 0000000000..2b9344f463 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_contains_iframe.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<iframe src="http://example.org/tests/toolkit/components/extensions/test/mochitest/file_contains_img.html"> +</iframe> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_contains_img.html b/toolkit/components/extensions/test/mochitest/file_contains_img.html new file mode 100644 index 0000000000..c1112acbd8 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_contains_img.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<img src="file_image_good.png"/> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html b/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html new file mode 100644 index 0000000000..6c1675cb47 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> + +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + <iframe id="emptyframe"></iframe> + <iframe id="regularframe" src="http://test1.example.com/"></iframe> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab2.html b/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab2.html new file mode 100644 index 0000000000..3b102b3d67 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab2.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> + +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + <iframe srcdoc="<iframe src='http://test1.example.com/'></iframe>"></iframe> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_contentscript_iframe.html b/toolkit/components/extensions/test/mochitest/file_contentscript_iframe.html new file mode 100644 index 0000000000..dda5169d69 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_contentscript_iframe.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> + +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + <iframe id="frame" src="http://test2.example.com/"></iframe> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_green.html b/toolkit/components/extensions/test/mochitest/file_green.html new file mode 100644 index 0000000000..20755c5b56 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_green.html @@ -0,0 +1,3 @@ +<meta charset=utf-8> +<title>Super green test page</title> +<body style="background: #0f0"> diff --git a/toolkit/components/extensions/test/mochitest/file_image_bad.png b/toolkit/components/extensions/test/mochitest/file_image_bad.png Binary files differnew file mode 100644 index 0000000000..4c3be50847 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_image_bad.png diff --git a/toolkit/components/extensions/test/mochitest/file_image_good.png b/toolkit/components/extensions/test/mochitest/file_image_good.png Binary files differnew file mode 100644 index 0000000000..769c636340 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_image_good.png diff --git a/toolkit/components/extensions/test/mochitest/file_image_great.png b/toolkit/components/extensions/test/mochitest/file_image_great.png Binary files differnew file mode 100644 index 0000000000..769c636340 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_image_great.png diff --git a/toolkit/components/extensions/test/mochitest/file_image_redirect.png b/toolkit/components/extensions/test/mochitest/file_image_redirect.png Binary files differnew file mode 100644 index 0000000000..4c3be50847 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_image_redirect.png diff --git a/toolkit/components/extensions/test/mochitest/file_indexedDB.html b/toolkit/components/extensions/test/mochitest/file_indexedDB.html new file mode 100644 index 0000000000..65b7e0ad2f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_indexedDB.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> +"use strict"; + +const objectStoreName = "Objects"; + +let test = {key: 0, value: "test"}; + +let request = indexedDB.open("WebExtensionTest", 1); +request.onupgradeneeded = event => { + let db = event.target.result; + let objectStore = db.createObjectStore(objectStoreName, + {autoIncrement: 0}); + request = objectStore.add(test.value, test.key); + request.onsuccess = event => { + db.close(); + window.postMessage("indexedDBCreated", "*"); + }; +}; + </script> + </head> + <body> + This is a test page. + </body> +<html> diff --git a/toolkit/components/extensions/test/mochitest/file_mixed.html b/toolkit/components/extensions/test/mochitest/file_mixed.html new file mode 100644 index 0000000000..f3c7dda580 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_mixed.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<div id="test">Sample text</div> +<img id="bad-image" src="http://example.com/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png" /> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_redirect_cors_bypass.html b/toolkit/components/extensions/test/mochitest/file_redirect_cors_bypass.html new file mode 100644 index 0000000000..b8fda2369a --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_redirect_cors_bypass.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>1450965 Skip Cors Check for Early WebExtention Redirects</title> +</head> +<body> + <pre id="c"> + Fetching ... + </pre> + <script> + "use strict"; + let c = document.querySelector("#c"); + const channel = new BroadcastChannel("test_bus"); + function l(t) { c.innerText += `${t}\n`; } + + fetch("https://example.org/tests/toolkit/components/extensions/test/mochitest/file_cors_blocked.txt") + .then(r => r.text()) + .then(t => { + // This Request should have been redirected to /file_sample.txt in + // onBeforeRequest. So the text should be 'Sample' + l(`Loaded: ${t}`); + channel.postMessage(t); + }).catch(e => { + // The Redirect Failed, most likly due to a CORS Error + l(`e`); + channel.postMessage(e.toString()); + }); + </script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_redirect_data_uri.html b/toolkit/components/extensions/test/mochitest/file_redirect_data_uri.html new file mode 100644 index 0000000000..fe8e5bea44 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_redirect_data_uri.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1434357: Allow Web Request API to redirect to data: URI</title> +</head> +<body> + <div id="testdiv">foo</div> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_remote_frame.html b/toolkit/components/extensions/test/mochitest/file_remote_frame.html new file mode 100644 index 0000000000..f1b9240092 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_remote_frame.html @@ -0,0 +1,20 @@ +<!DOCTYPE> +<html> + <head> + <meta charset="utf-8"> + <script> + "use strict"; + var response = { + tabs: false, + cookie: document.cookie, + }; + try { + browser.tabs.create({url: "file_sample.html"}); + response.tabs = true; + } catch (e) { + // ok + } + window.parent.postMessage(response, "*"); + </script> + </head> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_sample.html b/toolkit/components/extensions/test/mochitest/file_sample.html new file mode 100644 index 0000000000..a20e49a1f0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_sample.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<div id="test">Sample text</div> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_sample.txt b/toolkit/components/extensions/test/mochitest/file_sample.txt new file mode 100644 index 0000000000..c02cd532b1 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_sample.txt @@ -0,0 +1 @@ +Sample
\ No newline at end of file diff --git a/toolkit/components/extensions/test/mochitest/file_sample.txt^headers^ b/toolkit/components/extensions/test/mochitest/file_sample.txt^headers^ new file mode 100644 index 0000000000..cb762eff80 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_sample.txt^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: * diff --git a/toolkit/components/extensions/test/mochitest/file_script_bad.js b/toolkit/components/extensions/test/mochitest/file_script_bad.js new file mode 100644 index 0000000000..c425122c71 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_script_bad.js @@ -0,0 +1,3 @@ +"use strict"; + +window.failure = true; diff --git a/toolkit/components/extensions/test/mochitest/file_script_good.js b/toolkit/components/extensions/test/mochitest/file_script_good.js new file mode 100644 index 0000000000..14e959aa5c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_script_good.js @@ -0,0 +1,12 @@ +"use strict"; + +window.success = window.success ? window.success + 1 : 1; + +{ + let scripts = document.getElementsByTagName("script"); + let url = new URL(scripts[scripts.length - 1].src); + let flag = url.searchParams.get("q"); + if (flag) { + window.postMessage(flag, "*"); + } +} diff --git a/toolkit/components/extensions/test/mochitest/file_script_redirect.js b/toolkit/components/extensions/test/mochitest/file_script_redirect.js new file mode 100644 index 0000000000..c425122c71 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_script_redirect.js @@ -0,0 +1,3 @@ +"use strict"; + +window.failure = true; diff --git a/toolkit/components/extensions/test/mochitest/file_script_xhr.js b/toolkit/components/extensions/test/mochitest/file_script_xhr.js new file mode 100644 index 0000000000..ad01f74253 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_script_xhr.js @@ -0,0 +1,9 @@ +"use strict"; + +var request = new XMLHttpRequest(); +request.open( + "get", + "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/xhr_resource", + false +); +request.send(); diff --git a/toolkit/components/extensions/test/mochitest/file_serviceWorker.html b/toolkit/components/extensions/test/mochitest/file_serviceWorker.html new file mode 100644 index 0000000000..d2b99769cc --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_serviceWorker.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + "use strict"; + + navigator.serviceWorker.register("serviceWorker.js").then(() => { + window.postMessage("serviceWorkerRegistered", "*"); + }); + </script> + </head> + <body> + This is a test page. + </body> +<html> diff --git a/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_frame.html b/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_frame.html new file mode 100644 index 0000000000..909a1f9e36 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_frame.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<script> +"use strict"; + +let req = new XMLHttpRequest(); +req.open("GET", "/xhr_sandboxed"); +req.send(); + +let sandbox = document.createElement("iframe"); +sandbox.setAttribute("sandbox", "allow-scripts"); +sandbox.setAttribute("src", "file_simple_sandboxed_subframe.html"); +document.documentElement.appendChild(sandbox); +</script> +<img src="file_image_great.png"/> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_subframe.html b/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_subframe.html new file mode 100644 index 0000000000..a0a437d0eb --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_subframe.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_simple_xhr.html b/toolkit/components/extensions/test/mochitest/file_simple_xhr.html new file mode 100644 index 0000000000..f6ef67277d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_xhr.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<script> +"use strict"; + +let req = new XMLHttpRequest(); +req.open("GET", "http://example.org/example.txt"); +req.send(); +</script> +<img src="file_image_good.png"/> +<iframe src="file_simple_xhr_frame.html"/> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame.html b/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame.html new file mode 100644 index 0000000000..7f38247ac0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<script> +"use strict"; + +let req = new XMLHttpRequest(); +req.open("GET", "/xhr_resource"); +req.send(); +</script> +<img src="file_image_bad.png"/> +<iframe src="file_simple_xhr_frame2.html"/> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame2.html b/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame2.html new file mode 100644 index 0000000000..6174a0b402 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame2.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<script> +"use strict"; + +let req = new XMLHttpRequest(); +req.open("GET", "/xhr_resource_2"); +req.send(); + +let sandbox = document.createElement("iframe"); +sandbox.setAttribute("sandbox", "allow-scripts"); +sandbox.setAttribute("src", "file_simple_sandboxed_frame.html"); +document.documentElement.appendChild(sandbox); +</script> +<img src="file_image_redirect.png"/> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_streamfilter.txt b/toolkit/components/extensions/test/mochitest/file_streamfilter.txt new file mode 100644 index 0000000000..56cdd85e1d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_streamfilter.txt @@ -0,0 +1 @@ +Middle diff --git a/toolkit/components/extensions/test/mochitest/file_style_bad.css b/toolkit/components/extensions/test/mochitest/file_style_bad.css new file mode 100644 index 0000000000..8dbc8dc7a4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_style_bad.css @@ -0,0 +1,3 @@ +#test { + color: green !important; +} diff --git a/toolkit/components/extensions/test/mochitest/file_style_good.css b/toolkit/components/extensions/test/mochitest/file_style_good.css new file mode 100644 index 0000000000..46f9774b5f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_style_good.css @@ -0,0 +1,3 @@ +#test { + color: red; +} diff --git a/toolkit/components/extensions/test/mochitest/file_style_redirect.css b/toolkit/components/extensions/test/mochitest/file_style_redirect.css new file mode 100644 index 0000000000..8dbc8dc7a4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_style_redirect.css @@ -0,0 +1,3 @@ +#test { + color: green !important; +} diff --git a/toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html b/toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html new file mode 100644 index 0000000000..63f503ad3c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> + +<html> +<head> + <title>The Title</title> +</head> +<body> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html b/toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html new file mode 100644 index 0000000000..87ac7a2f64 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> + +<html> +<head> + <title>Another Title</title> + <link href="file_image_great.png" rel="icon" type="image/png" /> +</head> +<body> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_third_party.html b/toolkit/components/extensions/test/mochitest/file_third_party.html new file mode 100644 index 0000000000..fc5a326297 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_third_party.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<script> + +"use strict" + +let url = new URL(location); +let img = new Image(); +img.src = `http://${url.searchParams.get("domain")}/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png`; +document.body.appendChild(img); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_to_drawWindow.html b/toolkit/components/extensions/test/mochitest/file_to_drawWindow.html new file mode 100644 index 0000000000..6ebd54d9a3 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_to_drawWindow.html @@ -0,0 +1,9 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> +</head> +<body style="background: #ff9"> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect.html new file mode 100644 index 0000000000..cba3043f71 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> + +<html> + <head> + <meta http-equiv="refresh" content="1;dummy_page.html"> + </head> + <body> + </body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html new file mode 100644 index 0000000000..c5b436979f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html @@ -0,0 +1,8 @@ +<!DOCTYPE HTML> + +<html> + <head> + </head> + <body> + </body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^ b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^ new file mode 100644 index 0000000000..574a392a15 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^ @@ -0,0 +1 @@ +Refresh: 1;url=dummy_page.html diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_frameClientRedirect.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_frameClientRedirect.html new file mode 100644 index 0000000000..d360bcbb13 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_frameClientRedirect.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> + +<html> +<body> + +<iframe src="file_webNavigation_clientRedirect.html" width="200" height="200"></iframe> + +<form> +</form> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_frameRedirect.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_frameRedirect.html new file mode 100644 index 0000000000..06dbd43741 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_frameRedirect.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> + +<html> +<body> + +<iframe src="redirection.sjs" width="200" height="200"></iframe> + +<form> +</form> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe.html new file mode 100644 index 0000000000..307990714b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> + +<html> +<body> + +<iframe src="file_webNavigation_manualSubframe_page1.html" width="200" height="200"></iframe> + +<form> +</form> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page1.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page1.html new file mode 100644 index 0000000000..55bb7aa6ae --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page1.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> + +<html> + <body> + <h1>page1</h1> + <a href="file_webNavigation_manualSubframe_page2.html">page2</a> + </body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page2.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page2.html new file mode 100644 index 0000000000..8f589f8bbd --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page2.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> + +<html> + <body> + <h1>page2</h1> + </body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_with_about_blank.html b/toolkit/components/extensions/test/mochitest/file_with_about_blank.html new file mode 100644 index 0000000000..af51c2e52a --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_with_about_blank.html @@ -0,0 +1,10 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + <iframe id="a_b" src="about:blank"></iframe> + <iframe srcdoc="galactica actual" src="adama"></iframe> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_with_images.html b/toolkit/components/extensions/test/mochitest/file_with_images.html new file mode 100644 index 0000000000..6a3c090be2 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_with_images.html @@ -0,0 +1,10 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + <img src="https://example.com/chrome/toolkit/components/extensions/test/mochitest/file_image_good.png"> + <img src="http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest/file_image_great.png"> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_with_xorigin_frame.html b/toolkit/components/extensions/test/mochitest/file_with_xorigin_frame.html new file mode 100644 index 0000000000..d0d2f02e2d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_with_xorigin_frame.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<meta charset="utf-8"> + +<img src="file_image_great.png"/> +Load a cross-origin iframe from example.net <p> +<iframe src="http://example.net/tests/toolkit/components/extensions/test/mochitest/file_sample.html"></iframe> diff --git a/toolkit/components/extensions/test/mochitest/head.js b/toolkit/components/extensions/test/mochitest/head.js new file mode 100644 index 0000000000..2d26de34c7 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head.js @@ -0,0 +1,123 @@ +"use strict"; + +/* exported AppConstants, Assert */ + +var { AppConstants } = SpecialPowers.Cu.import( + "resource://gre/modules/AppConstants.jsm", + {} +); + +let remote = SpecialPowers.getBoolPref("extensions.webextensions.remote"); +if (remote) { + // We don't want to reset this at the end of the test, so that we don't have + // to spawn a new extension child process for each test unit. + SpecialPowers.setIntPref("dom.ipc.keepProcessesAlive.extension", 1); +} + +{ + let chromeScript = SpecialPowers.loadChromeScript( + SimpleTest.getTestFileURL("chrome_cleanup_script.js") + ); + + SimpleTest.registerCleanupFunction(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + + chromeScript.sendAsyncMessage("check-cleanup"); + + let results = await chromeScript.promiseOneMessage("cleanup-results"); + chromeScript.destroy(); + + if (results.extraWindows.length || results.extraTabs.length) { + ok( + false, + `Test left extra windows or tabs: ${JSON.stringify(results)}\n` + ); + } + }); +} + +let Assert = { + // Cut-down version based on Assert.jsm. Only supports regexp and objects as + // the expected variables. + rejects(promise, expected, msg) { + return promise.then( + () => { + ok(false, msg); + }, + actual => { + let matched = false; + if (Object.prototype.toString.call(expected) == "[object RegExp]") { + if (expected.test(actual)) { + matched = true; + } + } else if (actual instanceof expected) { + matched = true; + } + + if (matched) { + ok(true, msg); + } else { + ok(false, `Unexpected exception for "${msg}": ${actual}`); + } + } + ); + }, +}; + +/* exported waitForLoad */ + +function waitForLoad(win) { + return new Promise(resolve => { + win.addEventListener( + "load", + function() { + resolve(); + }, + { capture: true, once: true } + ); + }); +} + +/* exported loadChromeScript */ +function loadChromeScript(fn) { + let wrapper = ` +const {Services} = Cu.import("resource://gre/modules/Services.jsm", {}); +(${fn.toString()})();`; + + return SpecialPowers.loadChromeScript(new Function(wrapper)); +} + +/* exported consoleMonitor */ +let consoleMonitor = { + start(messages) { + this.chromeScript = SpecialPowers.loadChromeScript( + SimpleTest.getTestFileURL("mochitest_console.js") + ); + this.chromeScript.sendAsyncMessage("consoleStart", messages); + }, + + async finished() { + let done = this.chromeScript.promiseOneMessage("consoleDone").then(done => { + this.chromeScript.destroy(); + return done; + }); + this.chromeScript.sendAsyncMessage("waitForConsole"); + let test = await done; + ok(test.ok, test.message); + }, +}; +/* exported waitForState */ + +function waitForState(sw, state) { + return new Promise(resolve => { + if (sw.state === state) { + return resolve(); + } + sw.addEventListener("statechange", function onStateChange() { + if (sw.state === state) { + sw.removeEventListener("statechange", onStateChange); + resolve(); + } + }); + }); +} diff --git a/toolkit/components/extensions/test/mochitest/head_cookies.js b/toolkit/components/extensions/test/mochitest/head_cookies.js new file mode 100644 index 0000000000..610c800c94 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head_cookies.js @@ -0,0 +1,287 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* exported testCookies */ +/* import-globals-from head.js */ + +async function testCookies(options) { + // Changing the options object is a bit of a hack, but it allows us to easily + // pass an expiration date to the background script. + options.expiry = Date.now() / 1000 + 3600; + + async function background(backgroundOptions) { + // Ask the parent scope to change some cookies we may or may not have + // permission for. + let awaitChanges = new Promise(resolve => { + browser.test.onMessage.addListener(msg => { + browser.test.assertEq("cookies-changed", msg, "browser.test.onMessage"); + resolve(); + }); + }); + + let changed = []; + browser.cookies.onChanged.addListener(event => { + changed.push(`${event.cookie.name}:${event.cause}`); + }); + browser.test.sendMessage("change-cookies"); + + // Try to access some cookies in various ways. + let { url, domain, secure } = backgroundOptions; + + let failures = 0; + let tallyFailure = error => { + failures++; + }; + + try { + await awaitChanges; + + let cookie = await browser.cookies.get({ url, name: "foo" }); + browser.test.assertEq( + backgroundOptions.shouldPass, + cookie != null, + "should pass == get cookie" + ); + + let cookies = await browser.cookies.getAll({ domain }); + if (backgroundOptions.shouldPass) { + browser.test.assertEq(2, cookies.length, "expected number of cookies"); + } else { + browser.test.assertEq(0, cookies.length, "expected number of cookies"); + } + + await Promise.all([ + browser.cookies + .set({ + url, + domain, + secure, + name: "foo", + value: "baz", + expirationDate: backgroundOptions.expiry, + }) + .catch(tallyFailure), + browser.cookies + .set({ + url, + domain, + secure, + name: "bar", + value: "quux", + expirationDate: backgroundOptions.expiry, + }) + .catch(tallyFailure), + browser.cookies.remove({ url, name: "deleted" }), + ]); + + if (backgroundOptions.shouldPass) { + // The order of eviction events isn't guaranteed, so just check that + // it's there somewhere. + let evicted = changed.indexOf("evicted:evicted"); + if (evicted < 0) { + browser.test.fail("got no eviction event"); + } else { + browser.test.succeed("got eviction event"); + changed.splice(evicted, 1); + } + + browser.test.assertEq( + "x:explicit,x:overwrite,x:explicit,x:explicit,foo:overwrite,foo:explicit,bar:explicit,deleted:explicit", + changed.join(","), + "expected changes" + ); + } else { + browser.test.assertEq("", changed.join(","), "expected no changes"); + } + + if (!(backgroundOptions.shouldPass || backgroundOptions.shouldWrite)) { + browser.test.assertEq(2, failures, "Expected failures"); + } else { + browser.test.assertEq(0, failures, "Expected no failures"); + } + + browser.test.notifyPass("cookie-permissions"); + } catch (error) { + browser.test.fail(`Error: ${error} :: ${error.stack}`); + browser.test.notifyFail("cookie-permissions"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: options.permissions, + }, + + background: `(${background})(${JSON.stringify(options)})`, + }); + + let stepOne = loadChromeScript(() => { + const { addMessageListener, sendAsyncMessage } = this; + addMessageListener("options", options => { + let domain = options.domain.replace(/^\.?/, "."); + // This will be evicted after we add a fourth cookie. + Services.cookies.add( + domain, + "/", + "evicted", + "bar", + options.secure, + false, + false, + options.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + options.url.startsWith("https") + ? Ci.nsICookie.SCHEME_HTTPS + : Ci.nsICookie.SCHEME_HTTP + ); + // This will be modified by the background script. + Services.cookies.add( + domain, + "/", + "foo", + "bar", + options.secure, + false, + false, + options.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + options.url.startsWith("https") + ? Ci.nsICookie.SCHEME_HTTPS + : Ci.nsICookie.SCHEME_HTTP + ); + // This will be deleted by the background script. + Services.cookies.add( + domain, + "/", + "deleted", + "bar", + options.secure, + false, + false, + options.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + options.url.startsWith("https") + ? Ci.nsICookie.SCHEME_HTTPS + : Ci.nsICookie.SCHEME_HTTP + ); + sendAsyncMessage("done"); + }); + }); + stepOne.sendAsyncMessage("options", options); + await stepOne.promiseOneMessage("done"); + stepOne.destroy(); + + await extension.startup(); + + await extension.awaitMessage("change-cookies"); + + let stepTwo = loadChromeScript(() => { + const { addMessageListener, sendAsyncMessage } = this; + addMessageListener("options", options => { + let domain = options.domain.replace(/^\.?/, "."); + + Services.cookies.add( + domain, + "/", + "x", + "y", + options.secure, + false, + false, + options.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + options.url.startsWith("https") + ? Ci.nsICookie.SCHEME_HTTPS + : Ci.nsICookie.SCHEME_HTTP + ); + Services.cookies.add( + domain, + "/", + "x", + "z", + options.secure, + false, + false, + options.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + options.url.startsWith("https") + ? Ci.nsICookie.SCHEME_HTTPS + : Ci.nsICookie.SCHEME_HTTP + ); + Services.cookies.remove(domain, "x", "/", {}); + sendAsyncMessage("done"); + }); + }); + stepTwo.sendAsyncMessage("options", options); + await stepTwo.promiseOneMessage("done"); + stepTwo.destroy(); + + extension.sendMessage("cookies-changed"); + + await extension.awaitFinish("cookie-permissions"); + await extension.unload(); + + let stepThree = loadChromeScript(() => { + const { addMessageListener, sendAsyncMessage, assert } = this; + let cookieSvc = Services.cookies; + + function getCookies(host) { + let cookies = []; + for (let cookie of cookieSvc.getCookiesFromHost(host, {})) { + cookies.push(cookie); + } + return cookies.sort((a, b) => a.name.localeCompare(b.name)); + } + + addMessageListener("options", options => { + let cookies = getCookies(options.domain); + + if (options.shouldPass) { + assert.equal(cookies.length, 2, "expected two cookies for host"); + + assert.equal(cookies[0].name, "bar", "correct cookie name"); + assert.equal(cookies[0].value, "quux", "correct cookie value"); + + assert.equal(cookies[1].name, "foo", "correct cookie name"); + assert.equal(cookies[1].value, "baz", "correct cookie value"); + } else if (options.shouldWrite) { + // Note: |shouldWrite| applies only when |shouldPass| is false. + // This is necessary because, unfortunately, websites (and therefore web + // extensions) are allowed to write some cookies which they're not allowed + // to read. + assert.equal(cookies.length, 3, "expected three cookies for host"); + + assert.equal(cookies[0].name, "bar", "correct cookie name"); + assert.equal(cookies[0].value, "quux", "correct cookie value"); + + assert.equal(cookies[1].name, "deleted", "correct cookie name"); + + assert.equal(cookies[2].name, "foo", "correct cookie name"); + assert.equal(cookies[2].value, "baz", "correct cookie value"); + } else { + assert.equal(cookies.length, 2, "expected two cookies for host"); + + assert.equal(cookies[0].name, "deleted", "correct second cookie name"); + + assert.equal(cookies[1].name, "foo", "correct cookie name"); + assert.equal(cookies[1].value, "bar", "correct cookie value"); + } + + for (let cookie of cookies) { + cookieSvc.remove(cookie.host, cookie.name, "/", {}); + } + // Make sure we don't silently poison subsequent tests if something goes wrong. + assert.equal(getCookies(options.domain).length, 0, "cookies cleared"); + sendAsyncMessage("done"); + }); + }); + stepThree.sendAsyncMessage("options", options); + await stepThree.promiseOneMessage("done"); + stepThree.destroy(); +} diff --git a/toolkit/components/extensions/test/mochitest/head_notifications.js b/toolkit/components/extensions/test/mochitest/head_notifications.js new file mode 100644 index 0000000000..0c8cf24350 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head_notifications.js @@ -0,0 +1,169 @@ +"use strict"; + +/* exported MockAlertsService */ + +function mockServicesChromeScript() { + const MOCK_ALERTS_CID = Components.ID( + "{48068bc2-40ab-4904-8afd-4cdfb3a385f3}" + ); + const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1"; + + const { setTimeout } = ChromeUtils.import( + "resource://gre/modules/Timer.jsm", + {} + ); + const registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + + let activeNotifications = Object.create(null); + + const mockAlertsService = { + showPersistentNotification: function(persistentData, alert, alertListener) { + this.showAlert(alert, alertListener); + }, + + showAlert: function(alert, listener) { + activeNotifications[alert.name] = { + listener: listener, + cookie: alert.cookie, + title: alert.title, + }; + + // fake async alert show event + if (listener) { + setTimeout(function() { + listener.observe(null, "alertshow", alert.cookie); + }, 100); + } + }, + + showAlertNotification: function( + imageUrl, + title, + text, + textClickable, + cookie, + alertListener, + name + ) { + this.showAlert( + { + name: name, + cookie: cookie, + title: title, + }, + alertListener + ); + }, + + closeAlert: function(name) { + let alertNotification = activeNotifications[name]; + if (alertNotification) { + if (alertNotification.listener) { + alertNotification.listener.observe( + null, + "alertfinished", + alertNotification.cookie + ); + } + delete activeNotifications[name]; + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIAlertsService"]), + + createInstance: function(outer, iid) { + if (outer != null) { + throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION); + } + return this.QueryInterface(iid); + }, + }; + + registrar.registerFactory( + MOCK_ALERTS_CID, + "alerts service", + ALERTS_SERVICE_CONTRACT_ID, + mockAlertsService + ); + + function clickNotifications(doClose) { + // Until we need to close a specific notification, just click them all. + for (let [name, notification] of Object.entries(activeNotifications)) { + let { listener, cookie } = notification; + listener.observe(null, "alertclickcallback", cookie); + if (doClose) { + mockAlertsService.closeAlert(name); + } + } + } + + function closeAllNotifications() { + for (let alertName of Object.keys(activeNotifications)) { + mockAlertsService.closeAlert(alertName); + } + } + + const { addMessageListener, sendAsyncMessage } = this; + + addMessageListener("mock-alert-service:unregister", () => { + closeAllNotifications(); + activeNotifications = null; + registrar.unregisterFactory(MOCK_ALERTS_CID, mockAlertsService); + sendAsyncMessage("mock-alert-service:unregistered"); + }); + + addMessageListener( + "mock-alert-service:click-notifications", + clickNotifications + ); + + addMessageListener( + "mock-alert-service:close-notifications", + closeAllNotifications + ); + + sendAsyncMessage("mock-alert-service:registered"); +} + +const MockAlertsService = { + async register() { + if (this._chromeScript) { + throw new Error("MockAlertsService already registered"); + } + this._chromeScript = SpecialPowers.loadChromeScript( + mockServicesChromeScript + ); + await this._chromeScript.promiseOneMessage("mock-alert-service:registered"); + }, + async unregister() { + if (!this._chromeScript) { + throw new Error("MockAlertsService not registered"); + } + this._chromeScript.sendAsyncMessage("mock-alert-service:unregister"); + return this._chromeScript + .promiseOneMessage("mock-alert-service:unregistered") + .then(() => { + this._chromeScript.destroy(); + this._chromeScript = null; + }); + }, + async clickNotifications() { + // Most implementations of the nsIAlertsService automatically close upon click. + await this._chromeScript.sendAsyncMessage( + "mock-alert-service:click-notifications", + true + ); + }, + async clickNotificationsWithoutClose() { + // The implementation on macOS does not automatically close the notification. + await this._chromeScript.sendAsyncMessage( + "mock-alert-service:click-notifications", + false + ); + }, + async closeNotifications() { + await this._chromeScript.sendAsyncMessage( + "mock-alert-service:close-notifications" + ); + }, +}; diff --git a/toolkit/components/extensions/test/mochitest/head_unlimitedStorage.js b/toolkit/components/extensions/test/mochitest/head_unlimitedStorage.js new file mode 100644 index 0000000000..73b98b68ae --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head_unlimitedStorage.js @@ -0,0 +1,50 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* exported checkSitePermissions */ + +const { Services } = SpecialPowers; +const { NetUtil } = SpecialPowers.Cu.import( + "resource://gre/modules/NetUtil.jsm", + {} +); + +function checkSitePermissions(uuid, expectedPermAction, assertMessage) { + if (!uuid) { + throw new Error( + "checkSitePermissions should not be called with an undefined uuid" + ); + } + + const baseURI = NetUtil.newURI(`moz-extension://${uuid}/`); + const principal = Services.scriptSecurityManager.createContentPrincipal( + baseURI, + {} + ); + + const sitePermissions = { + webextUnlimitedStorage: Services.perms.testPermissionFromPrincipal( + principal, + "WebExtensions-unlimitedStorage" + ), + indexedDB: Services.perms.testPermissionFromPrincipal( + principal, + "indexedDB" + ), + persistentStorage: Services.perms.testPermissionFromPrincipal( + principal, + "persistent-storage" + ), + }; + + for (const [sitePermissionName, actualPermAction] of Object.entries( + sitePermissions + )) { + is( + actualPermAction, + expectedPermAction, + `The extension "${sitePermissionName}" SitePermission ${assertMessage} as expected` + ); + } +} diff --git a/toolkit/components/extensions/test/mochitest/head_webrequest.js b/toolkit/components/extensions/test/mochitest/head_webrequest.js new file mode 100644 index 0000000000..f6c6530e41 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head_webrequest.js @@ -0,0 +1,482 @@ +"use strict"; + +let commonEvents = { + onBeforeRequest: [{ urls: ["<all_urls>"] }, ["blocking"]], + onBeforeSendHeaders: [ + { urls: ["<all_urls>"] }, + ["blocking", "requestHeaders"], + ], + onSendHeaders: [{ urls: ["<all_urls>"] }, ["requestHeaders"]], + onBeforeRedirect: [{ urls: ["<all_urls>"] }], + onHeadersReceived: [ + { urls: ["<all_urls>"] }, + ["blocking", "responseHeaders"], + ], + // Auth tests will need to set their own events object + // "onAuthRequired": [{urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]], + onResponseStarted: [{ urls: ["<all_urls>"] }], + onCompleted: [{ urls: ["<all_urls>"] }, ["responseHeaders"]], + onErrorOccurred: [{ urls: ["<all_urls>"] }], +}; + +function background(events) { + const IP_PATTERN = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; + + let expect; + let ignore; + let defaultOrigin; + let watchAuth = Object.keys(events).includes("onAuthRequired"); + let expectedIp = null; + + browser.test.onMessage.addListener((msg, expected) => { + if (msg !== "set-expected") { + return; + } + expect = expected.expect; + defaultOrigin = expected.origin; + ignore = expected.ignore; + let promises = []; + // Initialize some stuff we'll need in the tests. + for (let entry of Object.values(expect)) { + // a place for the test infrastructure to store some state. + entry.test = {}; + // Each entry in expected gets a Promise that will be resolved in the + // last event for that entry. This will either be onCompleted, or the + // last entry if an events list was provided. + promises.push( + new Promise(resolve => { + entry.test.resolve = resolve; + }) + ); + // If events was left undefined, we're expecting all normal events we're + // listening for, exclude onBeforeRedirect and onErrorOccurred + if (entry.events === undefined) { + entry.events = Object.keys(events).filter( + name => name != "onErrorOccurred" && name != "onBeforeRedirect" + ); + } + if (entry.optional_events === undefined) { + entry.optional_events = []; + } + } + // When every expected entry has finished our test is done. + Promise.all(promises).then(() => { + browser.test.sendMessage("done"); + }); + browser.test.sendMessage("continue"); + }); + + // Retrieve the per-file/test expected values. + function getExpected(details) { + let url = new URL(details.url); + let filename = url.pathname.split("/").pop(); + if (ignore && ignore.includes(filename)) { + return; + } + let expected = expect[filename]; + if (!expected) { + browser.test.fail(`unexpected request ${filename}`); + return; + } + // Save filename for redirect verification. + expected.test.filename = filename; + return expected; + } + + // Process any test header modifications that can happen in request or response phases. + // If a test includes headers, it needs a complete header object, no undefined + // objects even if empty: + // request: { + // add: {"HeaderName": "value",}, + // modify: {"HeaderName": "value",}, + // remove: ["HeaderName",], + // }, + // response: { + // add: {"HeaderName": "value",}, + // modify: {"HeaderName": "value",}, + // remove: ["HeaderName",], + // }, + function processHeaders(phase, expected, details) { + // This should only happen once per phase [request|response]. + browser.test.assertFalse( + !!expected.test[phase], + `First processing of headers for ${phase}` + ); + expected.test[phase] = true; + + let headers = details[`${phase}Headers`]; + browser.test.assertTrue( + Array.isArray(headers), + `${phase}Headers array present` + ); + + let { add, modify, remove } = expected.headers[phase]; + + for (let name in add) { + browser.test.assertTrue( + !headers.find(h => h.name === name), + `header ${name} to be added not present yet in ${phase}Headers` + ); + let header = { name: name }; + if (name.endsWith("-binary")) { + header.binaryValue = Array.from(add[name], c => c.charCodeAt(0)); + } else { + header.value = add[name]; + } + headers.push(header); + } + + let modifiedAny = false; + for (let header of headers) { + if (header.name.toLowerCase() in modify) { + header.value = modify[header.name.toLowerCase()]; + modifiedAny = true; + } + } + browser.test.assertTrue( + modifiedAny, + `at least one ${phase}Headers element to modify` + ); + + let deletedAny = false; + for (let j = headers.length; j-- > 0; ) { + if (remove.includes(headers[j].name.toLowerCase())) { + headers.splice(j, 1); + deletedAny = true; + } + } + browser.test.assertTrue( + deletedAny, + `at least one ${phase}Headers element to delete` + ); + + return headers; + } + + // phase is request or response. + function checkHeaders(phase, expected, details) { + if (!/^https?:/.test(details.url)) { + return; + } + + let headers = details[`${phase}Headers`]; + browser.test.assertTrue( + Array.isArray(headers), + `valid ${phase}Headers array` + ); + + let { add, modify, remove } = expected.headers[phase]; + for (let name in add) { + let value = headers.find(h => h.name.toLowerCase() === name.toLowerCase()) + .value; + browser.test.assertEq( + value, + add[name], + `header ${name} correctly injected in ${phase}Headers` + ); + } + + for (let name in modify) { + let value = headers.find(h => h.name.toLowerCase() === name.toLowerCase()) + .value; + browser.test.assertEq( + value, + modify[name], + `header ${name} matches modified value` + ); + } + + for (let name of remove) { + let found = headers.find( + h => h.name.toLowerCase() === name.toLowerCase() + ); + browser.test.assertFalse( + !!found, + `deleted header ${name} still found in ${phase}Headers` + ); + } + } + + let listeners = { + onBeforeRequest(expected, details, result) { + // Save some values to test request consistency in later events. + browser.test.assertTrue( + details.tabId !== undefined, + `tabId ${details.tabId}` + ); + browser.test.assertTrue( + details.requestId !== undefined, + `requestId ${details.requestId}` + ); + // Validate requestId if it's already set, this happens with redirects. + if (expected.test.requestId !== undefined) { + browser.test.assertEq( + "string", + typeof expected.test.requestId, + `requestid ${expected.test.requestId} is string` + ); + browser.test.assertEq( + "string", + typeof details.requestId, + `requestid ${details.requestId} is string` + ); + browser.test.assertEq( + "number", + typeof parseInt(details.requestId, 10), + "parsed requestid is number" + ); + browser.test.assertEq( + expected.test.requestId, + details.requestId, + "redirects will keep the same requestId" + ); + } else { + // Save any values we want to validate in later events. + expected.test.requestId = details.requestId; + expected.test.tabId = details.tabId; + } + // Tests we don't need to do every event. + browser.test.assertTrue( + details.type.toUpperCase() in browser.webRequest.ResourceType, + `valid resource type ${details.type}` + ); + if (details.type == "main_frame") { + browser.test.assertEq( + 0, + details.frameId, + "frameId is zero when type is main_frame, see bug 1329299" + ); + } + }, + onBeforeSendHeaders(expected, details, result) { + if (expected.headers && expected.headers.request) { + result.requestHeaders = processHeaders("request", expected, details); + } + if (expected.redirect) { + browser.test.log(`${name} redirect request`); + result.redirectUrl = details.url.replace( + expected.test.filename, + expected.redirect + ); + } + }, + onBeforeRedirect() {}, + onSendHeaders(expected, details, result) { + if (expected.headers && expected.headers.request) { + checkHeaders("request", expected, details); + } + }, + onResponseStarted() {}, + onHeadersReceived(expected, details, result) { + let expectedStatus = expected.status || 200; + // If authentication is being requested we don't fail on the status code. + if (watchAuth && [401, 407].includes(details.statusCode)) { + expectedStatus = details.statusCode; + } + browser.test.assertEq( + expectedStatus, + details.statusCode, + `expected HTTP status received for ${details.url} ${details.statusLine}` + ); + if (expected.headers && expected.headers.response) { + result.responseHeaders = processHeaders("response", expected, details); + } + }, + onAuthRequired(expected, details, result) { + result.authCredentials = expected.authInfo; + }, + onCompleted(expected, details, result) { + // If we have already completed a GET request for this url, + // and it was found, we expect for the response to come fromCache. + // expected.cached may be undefined, force boolean. + if (typeof expected.cached === "boolean") { + let expectCached = + expected.cached && + details.method === "GET" && + details.statusCode != 404; + browser.test.assertEq( + expectCached, + details.fromCache, + "fromCache is correct" + ); + } + // We can only tell IPs for non-cached HTTP requests. + if (!details.fromCache && /^https?:/.test(details.url)) { + browser.test.assertTrue( + IP_PATTERN.test(details.ip), + `IP for ${details.url} looks IP-ish: ${details.ip}` + ); + + // We can't easily predict the IP ahead of time, so just make + // sure they're all consistent. + expectedIp = expectedIp || details.ip; + browser.test.assertEq( + expectedIp, + details.ip, + `correct ip for ${details.url}` + ); + } + if (expected.headers && expected.headers.response) { + checkHeaders("response", expected, details); + } + }, + onErrorOccurred(expected, details, result) { + if (expected.error) { + if (Array.isArray(expected.error)) { + browser.test.assertTrue( + expected.error.includes(details.error), + "expected error message received in onErrorOccurred" + ); + } else { + browser.test.assertEq( + expected.error, + details.error, + "expected error message received in onErrorOccurred" + ); + } + } + }, + }; + + function getListener(name) { + return details => { + let result = {}; + browser.test.log(`${name} ${details.requestId} ${details.url}`); + let expected = getExpected(details); + if (!expected) { + return result; + } + let expectedEvent = expected.events[0] == name; + if (expectedEvent) { + expected.events.shift(); + } else { + // e10s vs. non-e10s errors can end with either onCompleted or onErrorOccurred + expectedEvent = expected.optional_events.includes(name); + } + browser.test.assertTrue(expectedEvent, `received ${name}`); + browser.test.assertEq( + expected.type, + details.type, + "resource type is correct" + ); + browser.test.assertEq( + expected.origin || defaultOrigin, + details.originUrl, + "origin is correct" + ); + + if (name != "onBeforeRequest") { + // On events after onBeforeRequest, check the previous values. + browser.test.assertEq( + expected.test.requestId, + details.requestId, + "correct requestId" + ); + browser.test.assertEq( + expected.test.tabId, + details.tabId, + "correct tabId" + ); + } + try { + listeners[name](expected, details, result); + } catch (e) { + browser.test.fail(`unexpected webrequest failure ${name} ${e}`); + } + + if (expected.cancel && expected.cancel == name) { + browser.test.log(`${name} cancel request`); + browser.test.sendMessage("cancelled"); + result.cancel = true; + } + // If we've used up all the events for this test, resolve the promise. + // If something wrong happens and more events come through, there will be + // failures. + if (expected.events.length <= 0) { + expected.test.resolve(); + } + return result; + }; + } + + for (let [name, args] of Object.entries(events)) { + browser.test.log(`adding listener for ${name}`); + try { + browser.webRequest[name].addListener(getListener(name), ...args); + } catch (e) { + browser.test.assertTrue( + /\brequestBody\b/.test(e.message), + "Request body is unsupported" + ); + + // RequestBody is disabled in release builds. + if (!/\brequestBody\b/.test(e.message)) { + throw e; + } + + args.splice(args.indexOf("requestBody"), 1); + browser.webRequest[name].addListener(getListener(name), ...args); + } + } +} + +/* exported makeExtension */ + +function makeExtension(events = commonEvents) { + return ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "webRequestBlocking", "<all_urls>"], + }, + background: `(${background})(${JSON.stringify(events)})`, + }); +} + +/* exported addStylesheet */ + +function addStylesheet(file) { + let link = document.createElement("link"); + link.setAttribute("rel", "stylesheet"); + link.setAttribute("href", file); + document.body.appendChild(link); +} + +/* exported addLink */ + +function addLink(file) { + let a = document.createElement("a"); + a.setAttribute("href", file); + a.setAttribute("target", "_blank"); + a.setAttribute("rel", "opener"); + document.body.appendChild(a); + return a; +} + +/* exported addImage */ + +function addImage(file) { + let img = document.createElement("img"); + img.setAttribute("src", file); + document.body.appendChild(img); +} + +/* exported addScript */ + +function addScript(file) { + let script = document.createElement("script"); + script.setAttribute("type", "text/javascript"); + script.setAttribute("src", file); + document + .getElementsByTagName("head") + .item(0) + .appendChild(script); +} + +/* exported addFrame */ + +function addFrame(file) { + let frame = document.createElement("iframe"); + frame.setAttribute("width", "200"); + frame.setAttribute("height", "200"); + frame.setAttribute("src", file); + document.body.appendChild(frame); +} diff --git a/toolkit/components/extensions/test/mochitest/hsts.sjs b/toolkit/components/extensions/test/mochitest/hsts.sjs new file mode 100644 index 0000000000..636f331882 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/hsts.sjs @@ -0,0 +1,8 @@ +function handleRequest(request, response) { + let page = "<!DOCTYPE html><html><body><p>HSTS page</p></body></html>"; + response.setStatusLine(request.httpVersion, "200", "OK"); + response.setHeader("Strict-Transport-Security", "max-age=60"); + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Content-Length", page.length + "", false); + response.write(page); +} diff --git a/toolkit/components/extensions/test/mochitest/mochitest-common.ini b/toolkit/components/extensions/test/mochitest/mochitest-common.ini new file mode 100644 index 0000000000..2e32a951e2 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini @@ -0,0 +1,206 @@ +[DEFAULT] +support-files = + chrome_cleanup_script.js + file_WebNavigation_page1.html + file_WebNavigation_page2.html + file_WebNavigation_page3.html + file_WebRequest_page3.html + file_contains_img.html + file_contains_iframe.html + file_green.html + file_contentscript_activeTab.html + file_contentscript_activeTab2.html + file_contentscript_iframe.html + file_image_bad.png + file_image_good.png + file_image_great.png + file_image_redirect.png + file_indexedDB.html + file_mixed.html + file_remote_frame.html + file_sample.html + file_sample.txt + file_sample.txt^headers^ + file_script_bad.js + file_script_good.js + file_script_redirect.js + file_script_xhr.js + file_serviceWorker.html + file_simple_sandboxed_frame.html + file_simple_sandboxed_subframe.html + file_simple_xhr.html + file_simple_xhr_frame.html + file_simple_xhr_frame2.html + file_streamfilter.txt + file_style_bad.css + file_style_good.css + file_style_redirect.css + file_third_party.html + file_to_drawWindow.html + file_webNavigation_clientRedirect.html + file_webNavigation_clientRedirect_httpHeaders.html + file_webNavigation_clientRedirect_httpHeaders.html^headers^ + file_webNavigation_frameClientRedirect.html + file_webNavigation_frameRedirect.html + file_webNavigation_manualSubframe.html + file_webNavigation_manualSubframe_page1.html + file_webNavigation_manualSubframe_page2.html + file_with_about_blank.html + file_with_xorigin_frame.html + head.js + head_cookies.js + head_notifications.js + head_unlimitedStorage.js + head_webrequest.js + hsts.sjs + mochitest_console.js + oauth.html + redirect_auto.sjs + redirection.sjs + return_headers.sjs + serviceWorker.js + slow_response.sjs + webrequest_worker.js + !/dom/tests/mochitest/geolocation/network_geolocation.sjs + !/toolkit/components/passwordmgr/test/authenticate.sjs + file_redirect_data_uri.html + file_redirect_cors_bypass.html + file_tabs_permission_page1.html + file_tabs_permission_page2.html +prefs = + security.mixed_content.upgrade_display_content=false + browser.chrome.guess_favicon=true + +[test_ext_activityLog.html] +skip-if = + os == 'android' + tsan # Times out on TSan, bug 1612707 + xorigin # Inconsistent pass/fail in opt and debug +[test_ext_async_clipboard.html] +skip-if = toolkit == 'android' || tsan # near-permafail after landing bug 1270059: Bug 1523131. tsan: bug 1612707 +[test_ext_background_canvas.html] +[test_ext_background_page.html] +skip-if = (toolkit == 'android') # android doesn't have devtools +[test_ext_browsingData_indexedDB.html] +[test_ext_browsingData_localStorage.html] +[test_ext_browsingData_pluginData.html] +[test_ext_browsingData_serviceWorkers.html] +[test_ext_browsingData_settings.html] +[test_ext_canvas_resistFingerprinting.html] +[test_ext_clipboard.html] +skip-if = os == 'android' +[test_ext_clipboard_image.html] +skip-if = headless # Bug 1405872 +[test_ext_contentscript_about_blank.html] +skip-if = os == 'android' # bug 1369440 +[test_ext_contentscript_activeTab.html] +skip-if = os == 'android' || fission +[test_ext_contentscript_cache.html] +skip-if = (os == 'linux' && debug) || (toolkit == 'android' && debug) # bug 1348241 +fail-if = xorigin # TypeError: can't access property "staticScripts", ext is undefined - Should not throw any errors +[test_ext_contentscript_canvas.html] +skip-if = (os == 'android') || (verify && debug && (os == 'linux')) # Bug 1617062 +[test_ext_contentscript_devtools_metadata.html] +[test_ext_contentscript_fission_frame.html] +[test_ext_contentscript_incognito.html] +skip-if = os == 'android' # Android does not support multiple windows. +[test_ext_contentscript_permission.html] +skip-if = tsan # Times out on TSan, bug 1612707 +[test_ext_cookies.html] +skip-if = os == 'android' || tsan # Times out on TSan intermittently, bug 1615184; not supported on Android yet +[test_ext_cookies_containers.html] +[test_ext_cookies_expiry.html] +[test_ext_cookies_first_party.html] +[test_ext_cookies_incognito.html] +skip-if = os == 'android' # Bug 1513544 Android does not support multiple windows. +[test_ext_cookies_permissions_bad.html] +[test_ext_cookies_permissions_good.html] +[test_ext_downloads_download.html] +[test_ext_embeddedimg_iframe_frameAncestors.html] +[test_ext_exclude_include_globs.html] +[test_ext_external_messaging.html] +[test_ext_generate.html] +[test_ext_geolocation.html] +skip-if = os == 'android' # Android support Bug 1336194 +[test_ext_identity.html] +skip-if = os == 'android' || tsan # unsupported. tsan: bug 1612707 +[test_ext_idle.html] +skip-if = tsan # Times out on TSan, bug 1612707 +[test_ext_inIncognitoContext_window.html] +skip-if = os == 'android' # Android does not support multiple windows. +[test_ext_listener_proxies.html] +[test_ext_new_tab_processType.html] +skip-if = verify && debug && (os == 'linux' || os == 'mac') +[test_ext_notifications.html] +skip-if = os == 'android' # Not supported on Android yet +[test_ext_protocolHandlers.html] +skip-if = (toolkit == 'android') # bug 1342577 +[test_ext_redirect_jar.html] +skip-if = os == 'win' && (debug || asan) # Bug 1563440 +[test_ext_request_urlClassification.html] +skip-if = os == 'android' # Bug 1615427 +[test_ext_runtime_connect.html] +[test_ext_runtime_connect_twoway.html] +[test_ext_runtime_connect2.html] +[test_ext_runtime_disconnect.html] +[test_ext_sendmessage_doublereply.html] +[test_ext_sendmessage_frameId.html] +[test_ext_sendmessage_no_receiver.html] +[test_ext_sendmessage_reply.html] +[test_ext_sendmessage_reply2.html] +skip-if = os == 'android' +[test_ext_storage_manager_capabilities.html] +skip-if = xorigin # JavaScript Error: "SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object" {file: "https://example.com/tests/SimpleTest/TestRunner.js" line: 157} +scheme=https +[test_ext_storage_smoke_test.html] +[test_ext_streamfilter_multiple.html] +skip-if = + !debug # Bug 1628642 + os == 'linux' # Bug 1628642 +[test_ext_streamfilter_processswitch.html] +[test_ext_subframes_privileges.html] +skip-if = os == 'android' || verify # bug 1489771 +[test_ext_tabs_captureTab.html] +[test_ext_tabs_query_popup.html] +[test_ext_tabs_permissions.html] +[test_ext_tabs_sendMessage.html] +[test_ext_test.html] +[test_ext_unlimitedStorage.html] +skip-if = os == 'android' +[test_ext_unlimitedStorage_legacy_persistent_indexedDB.html] +# IndexedDB persistent storage mode is not allowed on Fennec from a non-chrome privileged code +# (it has only been enabled for apps and privileged code). See Bug 1119462 for additional info. +skip-if = os == 'android' +[test_ext_web_accessible_resources.html] +skip-if = (os == 'android' && debug) || fission || (os == "linux" && bits == 64) # bug 1397615, bug 1588284, bug 1618231 +[test_ext_web_accessible_incognito.html] +skip-if = (os == 'android') || fission # Crashes intermittently: @ mozilla::dom::BrowsingContext::CreateFromIPC(mozilla::dom::BrowsingContext::IPCInitializer&&, mozilla::dom::BrowsingContextGroup*, mozilla::dom::ContentParent*), bug 1588284, bug 1397615 and bug 1513544 +[test_ext_webnavigation.html] +skip-if = (os == 'android' && debug) # bug 1397615 +[test_ext_webnavigation_filters.html] +skip-if = (os == 'android' && debug) || (verify && (os == 'linux' || os == 'mac')) # bug 1397615 +[test_ext_webnavigation_incognito.html] +skip-if = os == 'android' # bug 1513544 +[test_ext_webrequest_and_proxy_filter.html] +[test_ext_webrequest_auth.html] +skip-if = os == 'android' +[test_ext_webrequest_background_events.html] +[test_ext_webrequest_basic.html] +skip-if = + os == 'android' && debug # bug 1397615 + tsan # bug 1612707 + xorigin # JavaScript Error: "SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object" {file: "http://mochi.false-test:8888/tests/SimpleTest/TestRunner.js" line: 157}] +[test_ext_webrequest_errors.html] +skip-if = tsan +[test_ext_webrequest_filter.html] +skip-if = os == 'android' && debug || tsan # bug 1452348. tsan: bug 1612707 +[test_ext_webrequest_frameId.html] +skip-if = (webrender && os == 'linux') # Bug 1482983 caused by Bug 1480951 +[test_ext_webrequest_hsts.html] +skip-if = os == 'android' || os == 'linux' || os == 'mac' #Bug 1605515 +[test_ext_webrequest_upgrade.html] +[test_ext_webrequest_upload.html] +skip-if = os == 'android' # Currently fails in emulator tests +[test_ext_webrequest_redirect_bypass_cors.html] +[test_ext_webrequest_redirect_data_uri.html] +[test_ext_window_postMessage.html] diff --git a/toolkit/components/extensions/test/mochitest/mochitest-remote.ini b/toolkit/components/extensions/test/mochitest/mochitest-remote.ini new file mode 100644 index 0000000000..2828eb2182 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/mochitest-remote.ini @@ -0,0 +1,8 @@ +[DEFAULT] +tags = webextensions remote-webextensions +skip-if = !e10s || (os == 'android') # Bug 1620091: disable on android until extension process is done +prefs = + extensions.webextensions.remote=true + +[test_verify_remote_mode.html] +[include:mochitest-common.ini] diff --git a/toolkit/components/extensions/test/mochitest/mochitest.ini b/toolkit/components/extensions/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..4612cac657 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/mochitest.ini @@ -0,0 +1,12 @@ +[DEFAULT] +tags = webextensions in-process-webextensions +prefs = + extensions.webextensions.remote=false +dupe-manifest = true + +[test_verify_non_remote_mode.html] +[test_ext_storage_cleanup.html] +# Bug 1426514 storage_cleanup: clearing localStorage fails with oop + +[include:mochitest-common.ini] +skip-if = os == 'win' # Windows WebExtensions always run OOP diff --git a/toolkit/components/extensions/test/mochitest/mochitest_console.js b/toolkit/components/extensions/test/mochitest/mochitest_console.js new file mode 100644 index 0000000000..e4be8acd69 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/mochitest_console.js @@ -0,0 +1,53 @@ +"use strict"; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { addMessageListener, sendAsyncMessage } = this; + +// Much of the console monitoring code is copied from TestUtils but simplified +// to our needs. +function monitorConsole(msgs) { + function msgMatches(msg, pat) { + for (let k in pat) { + if (!(k in msg)) { + return false; + } + if (pat[k] instanceof RegExp && typeof msg[k] === "string") { + if (!pat[k].test(msg[k])) { + return false; + } + } else if (msg[k] !== pat[k]) { + return false; + } + } + return true; + } + + let counter = 0; + function listener(msg) { + if (msgMatches(msg, msgs[counter])) { + counter++; + } + } + addMessageListener("waitForConsole", () => { + sendAsyncMessage("consoleDone", { + ok: counter >= msgs.length, + message: `monitorConsole | messages left expected at least ${msgs.length} got ${counter}`, + }); + Services.console.unregisterListener(listener); + }); + + Services.console.registerListener(listener); +} + +addMessageListener("consoleStart", messages => { + for (let msg of messages) { + // Message might be a RegExp object from a different compartment, but + // instanceof RegExp will fail. If we have an object, lets just make + // sure. + let message = msg.message; + if (typeof message == "object" && !(message instanceof RegExp)) { + msg.message = new RegExp(message); + } + } + monitorConsole(messages); +}); diff --git a/toolkit/components/extensions/test/mochitest/oauth.html b/toolkit/components/extensions/test/mochitest/oauth.html new file mode 100644 index 0000000000..8b9b1d65ec --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/oauth.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> +<head> + <script> + "use strict"; + + onload = () => { + let url = new URL(location); + if (url.searchParams.get("post")) { + let server_redirect = `${url.searchParams.get("server_uri")}?redirect_uri=${encodeURIComponent(url.searchParams.get("redirect_uri"))}`; + let form = document.forms.testform; + form.setAttribute("action", server_redirect); + form.submit(); + } else { + let end = new URL(url.searchParams.get("redirect_uri")); + end.searchParams.set("access_token", "here ya go"); + location.href = end.href; + } + }; + </script> +</head> +<body> + <form name="testform" action="" method="POST"> + </form> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/redirect_auto.sjs b/toolkit/components/extensions/test/mochitest/redirect_auto.sjs new file mode 100644 index 0000000000..27d249f022 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/redirect_auto.sjs @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +Components.utils.importGlobalProperties(["URLSearchParams", "URL"]); + +function handleRequest(request, response) { + let params = new URLSearchParams(request.queryString); + if (params.has("no_redirect")) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write("ok"); + } else { + if (request.method == "POST") { + response.setStatusLine(request.httpVersion, 303, "Redirected"); + } else { + response.setStatusLine(request.httpVersion, 302, "Moved Temporarily"); + } + let url = new URL(params.get("redirect_uri") || params.get("default_redirect")); + url.searchParams.set("access_token", "here ya go"); + response.setHeader("Location", url.href); + } +} diff --git a/toolkit/components/extensions/test/mochitest/redirection.sjs b/toolkit/components/extensions/test/mochitest/redirection.sjs new file mode 100644 index 0000000000..370ecd213f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/redirection.sjs @@ -0,0 +1,4 @@ +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 302); + aResponse.setHeader("Location", "./dummy_page.html"); +} diff --git a/toolkit/components/extensions/test/mochitest/return_headers.sjs b/toolkit/components/extensions/test/mochitest/return_headers.sjs new file mode 100644 index 0000000000..54e2e5fb4d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/return_headers.sjs @@ -0,0 +1,20 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript sts=2 sw=2 et tw=80: */ +"use strict"; + +/* exported handleRequest */ + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain", false); + + let headers = {}; + // Why on earth is this a nsISimpleEnumerator... + let enumerator = request.headers; + while (enumerator.hasMoreElements()) { + let header = enumerator.getNext().data; + headers[header.toLowerCase()] = request.getHeader(header); + } + + response.write(JSON.stringify(headers)); +} + diff --git a/toolkit/components/extensions/test/mochitest/serviceWorker.js b/toolkit/components/extensions/test/mochitest/serviceWorker.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/serviceWorker.js diff --git a/toolkit/components/extensions/test/mochitest/slow_response.sjs b/toolkit/components/extensions/test/mochitest/slow_response.sjs new file mode 100644 index 0000000000..290d6ca1de --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/slow_response.sjs @@ -0,0 +1,55 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */ +"use strict"; + +/* eslint-disable no-unused-vars */ + +Cu.import("resource://gre/modules/AppConstants.jsm"); + +const DELAY = AppConstants.DEBUG ? 4000 : 800; + +let nsTimer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", "initWithCallback"); + +let timer; +function delay() { + return new Promise(resolve => { + timer = nsTimer(resolve, DELAY, Ci.nsITimer.TYPE_ONE_SHOT); + }); +} + +const PARTS = [ + `<!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <title></title> + </head> + <body>`, + "Lorem ipsum dolor sit amet, <br>", + "consectetur adipiscing elit, <br>", + "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. <br>", + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. <br>", + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. <br>", + "Excepteur sint occaecat cupidatat non proident, <br>", + "sunt in culpa qui officia deserunt mollit anim id est laborum.<br>", + ` + </body> + </html>`, +]; + +async function handleRequest(request, response) { + response.processAsync(); + + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "no-cache", false); + + await delay(); + + for (let part of PARTS) { + response.write(`${part}\n`); + await delay(); + } + + response.finish(); +} + diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_data_uri.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_data_uri.html new file mode 100644 index 0000000000..42950c50ec --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_data_uri.html @@ -0,0 +1,104 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test content script matching a data: URI</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> +</head> +<body> + +<script> +"use strict"; + +add_task(async function test_contentscript_data_uri() { + const target = ExtensionTestUtils.loadExtension({ + files: { + "page.html": `<!DOCTYPE html> + <meta charset="utf-8"> + <iframe id="inherited" src="data:text/html;charset=utf-8,inherited"></iframe> + `, + }, + background() { + browser.test.sendMessage("page", browser.runtime.getURL("page.html")); + }, + }); + + const scripts = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webNavigation"], + content_scripts: [{ + all_frames: true, + matches: ["<all_urls>"], + run_at: "document_start", + css: ["all_urls.css"], + js: ["all_urls.js"], + }], + }, + files: { + "all_urls.css": ` + body { background: yellow; } + `, + "all_urls.js": function() { + document.body.style.color = "red"; + browser.test.assertTrue(location.protocol !== "data:", + `Matched document not a data URI: ${location.href}`); + }, + }, + background() { + browser.webNavigation.onCompleted.addListener(({url, frameId}) => { + browser.test.log(`Document loading complete: ${url}`); + if (frameId === 0) { + browser.test.sendMessage("tab-ready", url); + } + }); + }, + }); + + await target.startup(); + await scripts.startup(); + + // Test extension page with a data: iframe. + const page = await target.awaitMessage("page"); + + // Hold on to the tab by the browser, as extension loads are COOP loads, and + // will break WindowProxy references. + let win = window.open(); + const browserFrame = win.browsingContext.embedderElement; + win.location.href = page; + + await scripts.awaitMessage("tab-ready"); + win = browserFrame.contentWindow; + is(win.location.href, page, "Extension page loaded into a tab"); + is(win.document.readyState, "complete", "Page finished loading"); + + const iframe = win.document.getElementById("inherited").contentWindow; + is(iframe.document.readyState, "complete", "iframe finished loading"); + + const style1 = iframe.getComputedStyle(iframe.document.body); + is(style1.color, "rgb(0, 0, 0)", "iframe text color is unmodified"); + is(style1.backgroundColor, "rgba(0, 0, 0, 0)", "iframe background unmodified"); + + // Test extension tab navigated to a data: URI. + const data = "data:text/html;charset=utf-8,also-inherits"; + win.location.href = data; + + await scripts.awaitMessage("tab-ready"); + win = browserFrame.contentWindow; + is(win.location.href, data, "Extension tab navigated to a data: URI"); + is(win.document.readyState, "complete", "Tab finished loading"); + + const style2 = win.getComputedStyle(win.document.body); + is(style2.color, "rgb(0, 0, 0)", "Tab text color is unmodified"); + is(style2.backgroundColor, "rgba(0, 0, 0, 0)", "Tab background unmodified"); + + win.close(); + await target.unload(); + await scripts.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_telemetry.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_telemetry.html new file mode 100644 index 0000000000..198b8e85cf --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_telemetry.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for telemetry for content script injection</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> +</head> +<body> + +<script> +"use strict"; + +const HISTOGRAM = "WEBEXT_CONTENT_SCRIPT_INJECTION_MS"; + +add_task(async function test_contentscript_telemetry() { + // Turn on telemetry and reset it to the previous state once the test is completed. + const telemetryCanRecordBase = SpecialPowers.Services.telemetry.canRecordBase; + SpecialPowers.Services.telemetry.canRecordBase = true; + SimpleTest.registerCleanupFunction(() => { + SpecialPowers.Services.telemetry.canRecordBase = telemetryCanRecordBase; + }); + + function background() { + browser.test.onMessage.addListener(() => { + browser.tabs.executeScript({code: 'browser.test.sendMessage("content-script-run");'}); + }); + } + + let extensionData = { + manifest: { + permissions: ["<all_urls>"], + }, + background, + }; + + let win = window.open("http://example.com/"); + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + let histogram = SpecialPowers.Services.telemetry.getHistogramById(HISTOGRAM); + histogram.clear(); + is(histogram.snapshot().sum, 0, + `No data recorded for histogram: ${HISTOGRAM}.`); + + await extension.startup(); + is(histogram.snapshot().sum, 0, + `No data recorded for histogram after startup: ${HISTOGRAM}.`); + + extension.sendMessage(); + await extension.awaitMessage("content-script-run"); + + let histogramSum = histogram.snapshot().sum; + ok(histogramSum > 0, + `Data recorded for first extension for histogram: ${HISTOGRAM}.`); + + win.close(); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html new file mode 100644 index 0000000000..40403dea2b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script unrecognized property on manifest</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const BASE = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest"; + +add_task(async function test_contentscript() { + function background() { + browser.runtime.onMessage.addListener(async (msg) => { + if (msg == "loaded") { + // NOTE: we're removing the tab from here because doing a win.close() + // from the chrome test code is raising a "TypeError: can't access + // dead object" exception. + let tabs = await browser.tabs.query({active: true, currentWindow: true}); + await browser.tabs.remove(tabs[0].id); + + browser.test.notifyPass("content-script-loaded"); + } + }); + } + + function contentScript() { + chrome.runtime.sendMessage("loaded"); + } + + let extensionData = { + manifest: { + content_scripts: [ + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_idle", + "unrecognized_property": "with-a-random-value", + }, + ], + }, + background, + + files: { + "content_script.js": contentScript, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + SimpleTest.waitForExplicitFinish(); + let waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, [{ + message: /Reading manifest: Warning processing content_scripts.*.unrecognized_property: An unexpected property was found/, + }]); + }); + + ExtensionTestUtils.failOnSchemaWarnings(false); + await extension.startup(); + ExtensionTestUtils.failOnSchemaWarnings(true); + + window.open(`${BASE}/file_sample.html`); + + await Promise.all([extension.awaitFinish("content-script-loaded")]); + info("test page loaded"); + + await extension.unload(); + + SimpleTest.endMonitorConsole(); + await waitForConsole; +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_open.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_open.html new file mode 100644 index 0000000000..530937c1ac --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_open.html @@ -0,0 +1,114 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for permissions</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_downloads_open_permission() { + function backgroundScript() { + browser.test.assertEq(browser.downloads.open, undefined, + "`downloads.open` permission is required."); + browser.test.notifyPass("downloads tests"); + } + + let extensionData = { + background: backgroundScript, + manifest: { + permissions: ["downloads"], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await extension.awaitFinish("downloads tests"); + await extension.unload(); +}); + +add_task(async function test_downloads_open_requires_user_interaction() { + async function backgroundScript() { + await browser.test.assertRejects( + browser.downloads.open(10), + "downloads.open may only be called from a user input handler", + "The error is informative."); + + browser.test.notifyPass("downloads tests"); + } + + let extensionData = { + background: backgroundScript, + manifest: { + permissions: ["downloads", "downloads.open"], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await extension.awaitFinish("downloads tests"); + await extension.unload(); +}); + +add_task(async function downloads_open_invalid_id() { + async function pageScript() { + window.addEventListener("keypress", async function handler() { + try { + await browser.downloads.open(10); + browser.test.sendMessage("download-open.result", {success: true}); + } catch (e) { + browser.test.sendMessage("download-open.result", { + success: false, + error: e.message, + }); + } + window.removeEventListener("keypress", handler); + }); + + browser.test.sendMessage("page-ready"); + } + + let extensionData = { + background() { + browser.test.sendMessage("ready", browser.runtime.getURL("page.html")); + }, + files: { + "foo.txt": "It's the file called foo.txt.", + "page.html": `<html><head> + <script src="page.js"><\/script> + </head></html>`, + "page.js": pageScript, + }, + manifest: { + permissions: ["downloads", "downloads.open"], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let url = await extension.awaitMessage("ready"); + let win = window.open(); + let browserFrame = win.browsingContext.embedderElement; + win.location.href = url; + await extension.awaitMessage("page-ready"); + + synthesizeKey("a", {}, browserFrame.contentWindow); + let result = await extension.awaitMessage("download-open.result"); + + is(result.success, false, "Opening download fails."); + is(result.error, "Invalid download id 10", "The error is informative."); + + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html new file mode 100644 index 0000000000..64cfcfd289 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html @@ -0,0 +1,257 @@ +<!doctype html> +<html> +<head> + <title>Test downloads.download() saveAs option</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const {FileUtils} = ChromeUtils.import("resource://gre/modules/FileUtils.jsm"); + +const PROMPTLESS_DOWNLOAD_PREF = "browser.download.useDownloadDir"; + +const DOWNLOAD_FILENAME = "file_download.nonext.txt"; +const DEFAULT_SUBDIR = "subdir"; + +// We need to be able to distinguish files downloaded by the file picker from +// files downloaded without it. +let pickerDir; +let pbPickerDir; // for incognito downloads +let defaultDir; + +add_task(async function setup() { + // Reset DownloadLastDir preferences in case other tests set them. + SpecialPowers.Services.obs.notifyObservers( + null, + "browser:purge-session-history" + ); + + // Set up temporary directories. + let downloadDir = FileUtils.getDir("TmpD", ["downloads"]); + pickerDir = downloadDir.clone(); + pickerDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + info(`Using file picker download directory ${pickerDir.path}`); + pbPickerDir = downloadDir.clone(); + pbPickerDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + info(`Using private browsing file picker download directory ${pbPickerDir.path}`); + defaultDir = downloadDir.clone(); + defaultDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + info(`Using default download directory ${defaultDir.path}`); + let subDir = defaultDir.clone(); + subDir.append(DEFAULT_SUBDIR); + subDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + + isnot(pickerDir.path, defaultDir.path, + "Should be able to distinguish between files saved with or without the file picker"); + isnot(pickerDir.path, pbPickerDir.path, + "Should be able to distinguish between files saved in and out of private browsing mode"); + + await SpecialPowers.pushPrefEnv({"set": [ + ["browser.download.folderList", 2], + ["browser.download.dir", defaultDir.path], + ]}); + + SimpleTest.registerCleanupFunction(async () => { + await SpecialPowers.popPrefEnv(); + pickerDir.remove(true); + pbPickerDir.remove(true); + defaultDir.remove(true); // This also removes DEFAULT_SUBDIR. + }); +}); + +add_task(async function test_downloads_saveAs() { + const pickerFile = pickerDir.clone(); + pickerFile.append(DOWNLOAD_FILENAME); + + const pbPickerFile = pbPickerDir.clone(); + pbPickerFile.append(DOWNLOAD_FILENAME); + + const defaultFile = defaultDir.clone(); + defaultFile.append(DOWNLOAD_FILENAME); + + const {MockFilePicker} = SpecialPowers; + MockFilePicker.init(window); + + function mockFilePickerCallback(expectedStartingDir, pickedFile) { + return fp => { + // Assert that the downloads API correctly sets the starting directory. + ok(fp.displayDirectory.equals(expectedStartingDir), "Got the expected FilePicker displayDirectory"); + + // Assert that the downloads API configures both default properties. + is(fp.defaultString, DOWNLOAD_FILENAME, "Got the expected FilePicker defaultString"); + is(fp.defaultExtension, "txt", "Got the expected FilePicker defaultExtension"); + + MockFilePicker.setFiles([pickedFile]); + }; + } + + function background() { + const url = URL.createObjectURL(new Blob(["file content"])); + browser.test.onMessage.addListener(async (filename, saveAs, isPrivate) => { + try { + let options = { + url, + filename, + incognito: isPrivate, + }; + // Only define the saveAs option if the argument was actually set + if (saveAs !== undefined) { + options.saveAs = saveAs; + } + let id = await browser.downloads.download(options); + browser.downloads.onChanged.addListener(delta => { + if (delta.id == id && delta.state.current === "complete") { + browser.test.sendMessage("done", {ok: true, id}); + } + }); + } catch ({message}) { + browser.test.sendMessage("done", {ok: false, message}); + } + }); + browser.test.sendMessage("ready"); + } + + const manifest = { + background, + incognitoOverride: "spanning", + manifest: {permissions: ["downloads"]}, + }; + const extension = ExtensionTestUtils.loadExtension(manifest); + + await extension.startup(); + await extension.awaitMessage("ready"); + + // options should have the following properties: + // saveAs (Boolean or undefined) + // isPrivate (Boolean) + // fileName (string) + // expectedStartingDir (nsIFile) + // destinationFile (nsIFile) + async function testExpectFilePicker(options) { + ok(!options.destinationFile.exists(), "the file should have been cleaned up properly previously"); + + MockFilePicker.showCallback = mockFilePickerCallback( + options.expectedStartingDir, + options.destinationFile + ); + MockFilePicker.returnValue = MockFilePicker.returnOK; + + extension.sendMessage(options.fileName, options.saveAs, options.isPrivate); + let result = await extension.awaitMessage("done"); + ok(result.ok, `downloads.download() works with saveAs=${options.saveAs}`); + + ok(options.destinationFile.exists(), "the file exists."); + is(options.destinationFile.fileSize, 12, "downloaded file is the correct size"); + options.destinationFile.remove(false); + MockFilePicker.reset(); + + // Test the user canceling the save dialog. + MockFilePicker.returnValue = MockFilePicker.returnCancel; + + extension.sendMessage(options.fileName, options.saveAs, options.isPrivate); + result = await extension.awaitMessage("done"); + + ok(!result.ok, "download rejected if the user cancels the dialog"); + is(result.message, "Download canceled by the user", "with the correct message"); + ok(!options.destinationFile.exists(), "file was not downloaded"); + MockFilePicker.reset(); + } + + async function testNoFilePicker(saveAs) { + ok(!defaultFile.exists(), "the file should have been cleaned up properly previously"); + + extension.sendMessage(DOWNLOAD_FILENAME, saveAs, false); + let result = await extension.awaitMessage("done"); + ok(result.ok, `downloads.download() works with saveAs=${saveAs}`); + + ok(defaultFile.exists(), "the file exists."); + is(defaultFile.fileSize, 12, "downloaded file is the correct size"); + defaultFile.remove(false); + } + + info("Testing that saveAs=true uses the file picker as expected"); + let expectedStartingDir = defaultDir; + let fpOptions = { + saveAs: true, + isPrivate: false, + fileName: DOWNLOAD_FILENAME, + expectedStartingDir: expectedStartingDir, + destinationFile: pickerFile, + }; + await testExpectFilePicker(fpOptions); + + info("Testing that saveas=true reuses last file picker directory"); + fpOptions.expectedStartingDir = pickerDir; + await testExpectFilePicker(fpOptions); + + info("Testing that saveAs=true in PB reuses last directory"); + let nonPBStartingDir = fpOptions.expectedStartingDir; + fpOptions.isPrivate = true; + fpOptions.destinationFile = pbPickerFile; + await testExpectFilePicker(fpOptions); + + info("Testing that saveAs=true in PB uses a separate last directory"); + fpOptions.expectedStartingDir = pbPickerDir; + await testExpectFilePicker(fpOptions); + + info("Testing that saveAs=true in Permanent PB mode ignores the incognito option"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.privatebrowsing.autostart", true]], + }); + fpOptions.isPrivate = false; + fpOptions.expectedStartingDir = pbPickerDir; + await testExpectFilePicker(fpOptions); + + info("Testing that saveas=true reuses the non-PB last directory after private download"); + await SpecialPowers.popPrefEnv(); + fpOptions.isPrivate = false; + fpOptions.expectedStartingDir = nonPBStartingDir; + fpOptions.destinationFile = pickerFile; + await testExpectFilePicker(fpOptions); + + info("Testing that saveAs=true does not reuse last directory when filename contains a path separator"); + fpOptions.fileName = DEFAULT_SUBDIR + "/" + DOWNLOAD_FILENAME; + let destinationFile = defaultDir.clone(); + destinationFile.append(DEFAULT_SUBDIR); + fpOptions.expectedStartingDir = destinationFile.clone(); + destinationFile.append(DOWNLOAD_FILENAME); + fpOptions.destinationFile = destinationFile; + await testExpectFilePicker(fpOptions); + + info("Testing that saveAs=false does not use the file picker"); + fpOptions.saveAs = false; + await testNoFilePicker(fpOptions.saveAs); + + // When saveAs is not set, the behavior should be determined by the Firefox + // pref that normally determines whether the "Save As" prompt should be + // displayed. + info(`Testing that the file picker is used when saveAs is not specified ` + + `but ${PROMPTLESS_DOWNLOAD_PREF} is disabled`); + fpOptions.saveAs = undefined; + await SpecialPowers.pushPrefEnv({"set": [ + [PROMPTLESS_DOWNLOAD_PREF, false], + ]}); + await testExpectFilePicker(fpOptions); + + info(`Testing that the file picker is NOT used when saveAs is not ` + + `specified but ${PROMPTLESS_DOWNLOAD_PREF} is enabled`); + await SpecialPowers.popPrefEnv(); + await SpecialPowers.pushPrefEnv({"set": [ + [PROMPTLESS_DOWNLOAD_PREF, true], + ]}); + await testNoFilePicker(fpOptions.saveAs); + + await extension.unload(); + MockFilePicker.cleanup(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_uniquify.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_uniquify.html new file mode 100644 index 0000000000..b5fedee7ec --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_uniquify.html @@ -0,0 +1,116 @@ +<!doctype html> +<html> +<head> + <title>Test downloads.download() uniquify option</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const {FileUtils} = ChromeUtils.import("resource://gre/modules/FileUtils.jsm"); + +let directory; + +add_task(async function setup() { + directory = FileUtils.getDir("TmpD", ["downloads"]); + directory.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + info(`Using download directory ${directory.path}`); + + await SpecialPowers.pushPrefEnv({"set": [ + ["browser.download.folderList", 2], + ["browser.download.dir", directory.path], + ]}); + + SimpleTest.registerCleanupFunction(async () => { + await SpecialPowers.popPrefEnv(); + directory.remove(true); + }); +}); + +add_task(async function test_downloads_uniquify() { + const file = directory.clone(); + file.append("file_download.txt"); + + const unique = directory.clone(); + unique.append("file_download(1).txt"); + + const {MockFilePicker} = SpecialPowers; + MockFilePicker.init(window); + MockFilePicker.returnValue = MockFilePicker.returnOK; + + MockFilePicker.showCallback = fp => { + let file = directory.clone(); + file.append(fp.defaultString); + MockFilePicker.setFiles([file]); + }; + + function background() { + const url = URL.createObjectURL(new Blob(["file content"])); + browser.test.onMessage.addListener(async (filename, saveAs) => { + try { + let id = await browser.downloads.download({ + url, + filename, + saveAs, + conflictAction: "uniquify", + }); + browser.downloads.onChanged.addListener(delta => { + if (delta.id == id && delta.state.current === "complete") { + browser.test.sendMessage("done", {ok: true, id}); + } + }); + } catch ({message}) { + browser.test.sendMessage("done", {ok: false, message}); + } + }); + browser.test.sendMessage("ready"); + } + + const manifest = {background, manifest: {permissions: ["downloads"]}}; + const extension = ExtensionTestUtils.loadExtension(manifest); + + await extension.startup(); + await extension.awaitMessage("ready"); + + async function testUniquify(saveAs) { + info(`Testing conflictAction:"uniquify" with saveAs=${saveAs}`); + + ok(!file.exists(), "downloaded file should have been cleaned up before test ran"); + ok(!unique.exists(), "uniquified file should have been cleaned up before test ran"); + + // Test download without uniquify and create a conflicting file so we can + // test with uniquify. + extension.sendMessage("file_download.txt", saveAs); + let result = await extension.awaitMessage("done"); + ok(result.ok, "downloads.download() works with saveAs"); + + ok(file.exists(), "the file exists."); + is(file.fileSize, 12, "downloaded file is the correct size"); + + // Now that a conflicting file exists, test the uniquify behavior + extension.sendMessage("file_download.txt", saveAs); + result = await extension.awaitMessage("done"); + ok(result.ok, "downloads.download() works with saveAs and uniquify"); + + ok(unique.exists(), "the file exists."); + is(unique.fileSize, 12, "downloaded file is the correct size"); + + file.remove(false); + unique.remove(false); + } + await testUniquify(true); + await testUniquify(false); + + await extension.unload(); + MockFilePicker.cleanup(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_permissions.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_permissions.html new file mode 100644 index 0000000000..47761784b1 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_permissions.html @@ -0,0 +1,176 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for permissions</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function makeTest(manifestPermissions, optionalPermissions, checkFetch = true) { + return async function() { + function pageScript() { + /* global PERMISSIONS */ + /* eslint-disable mozilla/balanced-listeners */ + window.addEventListener("keypress", () => { + browser.permissions.request(PERMISSIONS).then(result => { + browser.test.sendMessage("request.result", result); + }, {once: true}); + }); + /* eslint-enable mozilla/balanced-listeners */ + + browser.test.onMessage.addListener(async msg => { + if (msg == "set-cookie") { + try { + await browser.cookies.set({ + url: "http://example.com/", + name: "COOKIE", + value: "NOM NOM", + }); + browser.test.sendMessage("set-cookie.result", {success: true}); + } catch (err) { + dump(`set cookie failed with ${err.message}\n`); + browser.test.sendMessage("set-cookie.result", + {success: false, message: err.message}); + } + } else if (msg == "remove") { + browser.permissions.remove(PERMISSIONS).then(result => { + browser.test.sendMessage("remove.result", result); + }); + } + }); + + browser.test.sendMessage("page-ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.test.sendMessage("ready", browser.runtime.getURL("page.html")); + }, + + manifest: { + permissions: manifestPermissions, + optional_permissions: [...(optionalPermissions.permissions || []), + ...(optionalPermissions.origins || [])], + + content_scripts: [{ + matches: ["http://mochi.test/*/file_sample.html"], + js: ["content_script.js"], + }], + }, + + files: { + "content_script.js": async () => { + let url = new URL(window.location.pathname, "http://example.com/"); + fetch(url, {}).then(response => { + browser.test.sendMessage("fetch.result", response.ok); + }).catch(err => { + browser.test.sendMessage("fetch.result", false); + }); + }, + + "page.html": `<html><head> + <script src="page.js"><\/script> + </head></html>`, + + "page.js": `const PERMISSIONS = ${JSON.stringify(optionalPermissions)}; (${pageScript})();`, + }, + }); + + await extension.startup(); + + function call(method) { + extension.sendMessage(method); + return extension.awaitMessage(`${method}.result`); + } + + let base = window.location.href.replace(/^chrome:\/\/mochitests\/content/, + "http://mochi.test:8888"); + let file = new URL("file_sample.html", base); + + async function testContentScript() { + let win = window.open(file); + let result = await extension.awaitMessage("fetch.result"); + win.close(); + return result; + } + + let url = await extension.awaitMessage("ready"); + let win = window.open(); + let browserFrame = win.browsingContext.embedderElement; + win.location.href = url; + await extension.awaitMessage("page-ready"); + + // Using the cookies API from an extension page should fail + let result = await call("set-cookie"); + is(result.success, false, "setting cookie failed"); + if (manifestPermissions.includes("cookies")) { + ok(/^Permission denied/.test(result.message), + "setting cookie failed with an appropriate error due to missing host permission"); + } else { + ok(/browser\.cookies is undefined/.test(result.message), + "setting cookie failed since cookies API is not present"); + } + + // Making a cross-origin request from a content script should fail + if (checkFetch) { + result = await testContentScript(); + is(result, false, "fetch() failed from content script due to lack of host permission"); + } + + synthesizeKey("a", {}, browserFrame.contentWindow); + result = await extension.awaitMessage("request.result"); + is(result, true, "permissions.request() succeeded"); + + // Using the cookies API from an extension page should succeed + result = await call("set-cookie"); + is(result.success, true, "setting cookie succeeded"); + + // Making a cross-origin request from a content script should succeed + if (checkFetch) { + result = await testContentScript(); + is(result, true, "fetch() succeeded from content script due to lack of host permission"); + } + + // Now revoke our permissions + result = await call("remove"); + + // The cookies API should once again fail + result = await call("set-cookie"); + is(result.success, false, "setting cookie failed"); + + // As should the cross-origin request from a content script + if (checkFetch) { + result = await testContentScript(); + is(result, false, "fetch() failed from content script due to lack of host permission"); + } + + await extension.unload(); + }; +} + +add_task(function setup() { + // Don't bother with prompts in this test. + return SpecialPowers.pushPrefEnv({ + set: [["extensions.webextOptionalPermissionPrompts", false]], + }); +}); + +const ORIGIN = "*://example.com/"; +add_task(makeTest([], { + permissions: ["cookies"], + origins: [ORIGIN], +})); + +add_task(makeTest(["cookies"], {origins: [ORIGIN]})); +add_task(makeTest([ORIGIN], {permissions: ["cookies"]}, false)); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_trackingprotection.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_trackingprotection.html new file mode 100644 index 0000000000..580ea5e793 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_trackingprotection.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +var {UrlClassifierTestUtils} = ChromeUtils.import("resource://testing-common/UrlClassifierTestUtils.jsm"); + +function tp_background(expectFail = true) { + fetch("https://tracking.example.com/example.txt").then(() => { + browser.test.assertTrue(!expectFail, "fetch received"); + browser.test.sendMessage("done"); + }, () => { + browser.test.assertTrue(expectFail, "fetch failure"); + browser.test.sendMessage("done"); + }); +} + +async function test_permission(permissions, expectFail) { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions, + }, + background: `(${tp_background})(${expectFail})`, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +} + +add_task(async function setup() { + await UrlClassifierTestUtils.addTestTrackers(); + await SpecialPowers.pushPrefEnv({ + set: [["privacy.trackingprotection.enabled", true]], + }); +}); + +// Fetch would be blocked with these tests +add_task(async function() { await test_permission([], true); }); +add_task(async function() { await test_permission(["http://*/"], true); }); +add_task(async function() { await test_permission(["http://*.example.com/"], true); }); +add_task(async function() { await test_permission(["http://localhost/*"], true); }); +// Fetch will not be blocked if the extension has host permissions. +add_task(async function() { await test_permission(["<all_urls>"], false); }); +add_task(async function() { await test_permission(["*://tracking.example.com/*"], false); }); + +add_task(async function test_contentscript() { + function contentScript() { + fetch("https://tracking.example.com/example.txt").then(() => { + browser.test.notifyPass("fetch received"); + }, () => { + browser.test.notifyFail("fetch failure"); + }); + } + + let extensionData = { + manifest: { + permissions: ["*://tracking.example.com/*"], + content_scripts: [ + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_start", + }, + ], + }, + + files: { + "content_script.js": contentScript, + }, + }; + const url = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest/file_sample.html"; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + let win = window.open(url); + await extension.awaitFinish(); + win.close(); + await extension.unload(); +}); + +add_task(async function teardown() { + UrlClassifierTestUtils.cleanupTestTrackers(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html new file mode 100644 index 0000000000..a06709d807 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function webnav_unresolved_uri_on_expected_URI_scheme() { + function background() { + let checkURLs; + + browser.webNavigation.onCompleted.addListener(async msg => { + if (checkURLs.length) { + let expectedURL = checkURLs.shift(); + browser.test.assertEq(expectedURL, msg.url, "Got the expected URL"); + await browser.tabs.remove(msg.tabId); + browser.test.sendMessage("next"); + } + }); + + browser.test.onMessage.addListener((name, urls) => { + if (name == "checkURLs") { + checkURLs = urls; + } + }); + + browser.test.sendMessage("ready", browser.runtime.getURL("/tab.html")); + } + + let extensionData = { + manifest: { + permissions: [ + "webNavigation", + ], + }, + background, + files: { + "tab.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + </html> + `, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + + let checkURLs = [ + "resource://gre/modules/Services.jsm", + "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js", + "about:mozilla", + ]; + + let tabURL = await extension.awaitMessage("ready"); + checkURLs.push(tabURL); + + extension.sendMessage("checkURLs", checkURLs); + + for (let url of checkURLs) { + window.open(url); + await extension.awaitMessage("next"); + } + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html new file mode 100644 index 0000000000..a9dfb0a902 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html @@ -0,0 +1,94 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const {webrequest_test} = ChromeUtils.import(SimpleTest.getTestFileURL("webrequest_test.jsm")); +let {testFetch, testXHR} = webrequest_test; + +// Here we test that any requests originating from a system principal are not +// accessible through WebRequest. text_ext_webrequest_background_events tests +// non-system principal requests. + +let testExtension = { + manifest: { + permissions: [ + "webRequest", + "<all_urls>", + ], + }, + background() { + let eventNames = [ + "onBeforeRequest", + "onBeforeSendHeaders", + "onSendHeaders", + "onHeadersReceived", + "onResponseStarted", + "onCompleted", + ]; + + function listener(name, details) { + // If we get anything, we failed. Removing the system principal check + // in ext-webrequest triggers this failure. + browser.test.fail(`received ${name}`); + } + + for (let name of eventNames) { + browser.webRequest[name].addListener( + listener.bind(null, name), + {urls: ["https://example.com/*"]} + ); + } + }, +}; + +add_task(async function test_webRequest_chromeworker_events() { + let extension = ExtensionTestUtils.loadExtension(testExtension); + await extension.startup(); + await new Promise(resolve => { + let worker = new ChromeWorker("webrequest_chromeworker.js"); + worker.onmessage = event => { + ok("chrome worker fetch finished"); + resolve(); + }; + worker.postMessage("go"); + }); + await extension.unload(); +}); + +add_task(async function test_webRequest_chromepage_events() { + let extension = ExtensionTestUtils.loadExtension(testExtension); + await extension.startup(); + await new Promise(resolve => { + fetch("https://example.com/example.txt").then(() => { + ok("test page loaded"); + resolve(); + }); + }); + await extension.unload(); +}); + +add_task(async function test_webRequest_jsm_events() { + let extension = ExtensionTestUtils.loadExtension(testExtension); + await extension.startup(); + await testFetch("https://example.com/example.txt").then(() => { + ok("fetch page loaded"); + }); + await testXHR("https://example.com/example.txt").then(() => { + ok("xhr page loaded"); + }); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_host_permissions.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_host_permissions.html new file mode 100644 index 0000000000..19c812f59f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_host_permissions.html @@ -0,0 +1,89 @@ +<!doctype html> +<html> +<head> + <title>Test webRequest checks host permissions</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_webRequest_host_permissions() { + function background() { + function png(details) { + browser.test.sendMessage("png", details.url); + } + browser.webRequest.onBeforeRequest.addListener(png, {urls: ["*://*/*.png"]}); + browser.test.sendMessage("ready"); + } + + const all = ExtensionTestUtils.loadExtension({background, manifest: {permissions: ["webRequest", "<all_urls>"]}}); + const example = ExtensionTestUtils.loadExtension({background, manifest: {permissions: ["webRequest", "https://example.com/"]}}); + const mochi_test = ExtensionTestUtils.loadExtension({background, manifest: {permissions: ["webRequest", "http://mochi.test/"]}}); + + await all.startup(); + await example.startup(); + await mochi_test.startup(); + + await all.awaitMessage("ready"); + await example.awaitMessage("ready"); + await mochi_test.awaitMessage("ready"); + + const win1 = window.open("https://example.com/chrome/toolkit/components/extensions/test/mochitest/file_with_images.html"); + let urls = [await all.awaitMessage("png"), + await all.awaitMessage("png")]; + ok(urls.some(url => url.endsWith("good.png")), "<all_urls> permission gets to see good.png"); + ok((await example.awaitMessage("png")).endsWith("good.png"), "example permission sees same-origin example.com image"); + ok(urls.some(url => url.endsWith("great.png")), "<all_urls> permission also sees great.png"); + + // Clear the in-memory image cache, it can prevent listeners from receiving events. + const imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools); + imgTools.getImgCacheForDocument(win1.document).clearCache(false); + win1.close(); + + const win2 = window.open("http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest/file_with_images.html"); + urls = [await all.awaitMessage("png"), + await all.awaitMessage("png")]; + ok(urls.some(url => url.endsWith("good.png")), "<all_urls> permission gets to see good.png"); + ok((await mochi_test.awaitMessage("png")).endsWith("great.png"), "mochi.test permission sees same-origin mochi.test image"); + ok(urls.some(url => url.endsWith("great.png")), "<all_urls> permission also sees great.png"); + win2.close(); + + await all.unload(); + await example.unload(); + await mochi_test.unload(); +}); + +add_task(async function test_webRequest_filter_permissions_warning() { + const manifest = { + permissions: ["webRequest", "http://example.com/"], + }; + + async function background() { + await browser.webRequest.onBeforeRequest.addListener(() => {}, {urls: ["http://example.org/"]}); + browser.test.notifyPass(); + } + + const extension = ExtensionTestUtils.loadExtension({manifest, background}); + + const warning = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, [{message: /filter doesn't overlap with host permissions/}]); + }); + + await extension.startup(); + await extension.awaitFinish(); + + SimpleTest.endMonitorConsole(); + await warning; + + await extension.unload(); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_mozextension.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_mozextension.html new file mode 100644 index 0000000000..4c19359d8b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_mozextension.html @@ -0,0 +1,193 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test moz-extension protocol use</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +let peakAchu; +add_task(async function setup() { + peakAchu = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "<all_urls>", + ], + }, + background() { + // ID for the extension in the tests. Try to observe it to ensure we cannot. + browser.webRequest.onBeforeRequest.addListener(details => { + browser.test.notifyFail(`PeakAchu onBeforeRequest ${details.url}`); + }, {urls: ["<all_urls>", "moz-extension://*/*"]}); + + browser.test.onMessage.addListener((msg, extensionUrl) => { + browser.test.log(`spying for ${extensionUrl}`); + browser.webRequest.onBeforeRequest.addListener(details => { + browser.test.notifyFail(`PeakAchu onBeforeRequest ${details.url}`); + }, {urls: [extensionUrl]}); + }); + }, + }); + await peakAchu.startup(); +}); + +add_task(async function test_webRequest_no_mozextension_permission() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "tabs", + "moz-extension://c9e007e0-e518-ed4c-8202-83849981dd21/*", + "moz-extension://*/*", + ], + }, + background() { + browser.test.notifyPass("loaded"); + }, + }); + + let messages = [ + {message: /processing permissions\.2: Value "moz-extension:\/\/c9e007e0-e518-ed4c-8202-83849981dd21\/\*"/}, + {message: /processing permissions\.3: Value "moz-extension:\/\/\*\/\*"/}, + ]; + + let waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, messages); + }); + + await extension.startup(); + await extension.awaitFinish("loaded"); + await extension.unload(); + + SimpleTest.endMonitorConsole(); + await waitForConsole; +}); + +add_task(async function test_webRequest_mozextension_fetch() { + function background() { + let page = browser.extension.getURL("fetched.html"); + browser.webRequest.onBeforeRequest.addListener(details => { + browser.test.assertEq(details.url, page, "got correct url in onBeforeRequest"); + browser.test.sendMessage("request-started"); + }, {urls: [browser.extension.getURL("*")]}, ["blocking"]); + browser.webRequest.onCompleted.addListener(details => { + browser.test.assertEq(details.url, page, "got correct url in onCompleted"); + browser.test.sendMessage("request-complete"); + }, {urls: [browser.extension.getURL("*")]}); + + browser.test.onMessage.addListener((msg, data) => { + fetch(page).then(() => { + browser.test.notifyPass("fetch success"); + browser.test.sendMessage("done"); + }, () => { + browser.test.fail("fetch failed"); + browser.test.sendMessage("done"); + }); + }); + browser.test.sendMessage("extensionUrl", browser.extension.getURL("*")); + } + + // Use webrequest to monitor moz-extension:// requests + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + "tabs", + "<all_urls>", + ], + }, + files: { + "fetched.html": ` + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h1>moz-extension file</h1> + </body> + </html> + `.trim(), + }, + background, + }); + + await extension.startup(); + // send the url for this extension to the monitoring extension + peakAchu.sendMessage("extensionUrl", await extension.awaitMessage("extensionUrl")); + + extension.sendMessage("testFetch"); + await extension.awaitMessage("request-started"); + await extension.awaitMessage("request-complete"); + await extension.awaitMessage("done"); + + await extension.unload(); +}); + +add_task(async function test_webRequest_mozextension_tab_query() { + function background() { + browser.test.sendMessage("extensionUrl", browser.extension.getURL("*")); + let page = browser.extension.getURL("tab.html"); + + async function onUpdated(tabId, tabInfo, tab) { + if (tabInfo.status !== "complete") { + return; + } + browser.test.log(`tab created ${tabId} ${JSON.stringify(tabInfo)} ${tab.url}`); + let tabs = await browser.tabs.query({url: browser.extension.getURL("*")}); + browser.test.assertEq(1, tabs.length, "got one tab"); + browser.test.assertEq(tabs.length && tabs[0].id, tab.id, "got the correct tab"); + browser.test.assertEq(tabs.length && tabs[0].url, page, "got correct url in tab"); + browser.tabs.remove(tabId); + browser.tabs.onUpdated.removeListener(onUpdated); + browser.test.sendMessage("tabs-done"); + } + browser.tabs.onUpdated.addListener(onUpdated); + browser.tabs.create({url: page}); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "tabs", + "<all_urls>", + ], + }, + files: { + "tab.html": ` + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h1>moz-extension file</h1> + </body> + </html> + `.trim(), + }, + background, + }); + + await extension.startup(); + peakAchu.sendMessage("extensionUrl", await extension.awaitMessage("extensionUrl")); + await extension.awaitMessage("tabs-done"); + await extension.unload(); +}); + +add_task(async function teardown() { + await peakAchu.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html b/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html new file mode 100644 index 0000000000..78359747ce --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm"); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +// Test that the default paths searched for native host manifests +// are the ones we expect. +add_task(async function test_default_paths() { + let expectUser, expectGlobal; + switch (AppConstants.platform) { + case "macosx": { + expectUser = OS.Path.join(OS.Constants.Path.homeDir, + "Library/Application Support/Mozilla"); + expectGlobal = "/Library/Application Support/Mozilla"; + + break; + } + + case "linux": { + expectUser = OS.Path.join(OS.Constants.Path.homeDir, ".mozilla"); + + const libdir = AppConstants.HAVE_USR_LIB64_DIR ? "lib64" : "lib"; + expectGlobal = OS.Path.join("/usr", libdir, "mozilla"); + break; + } + + default: + // Fixed filesystem paths are only defined for MacOS and Linux, + // there's nothing to test on other platforms. + ok(false, `This test does not apply on ${AppConstants.platform}`); + break; + } + + let userDir = Services.dirsvc.get("XREUserNativeManifests", Ci.nsIFile).path; + is(userDir, expectUser, "user-specific native messaging directory is correct"); + + let globalDir = Services.dirsvc.get("XRESysNativeManifests", Ci.nsIFile).path; + is(globalDir, expectGlobal, "system-wide native messaing directory is correct"); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_activityLog.html b/toolkit/components/extensions/test/mochitest/test_ext_activityLog.html new file mode 100644 index 0000000000..ce4689540d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_activityLog.html @@ -0,0 +1,390 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension activityLog test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_api() { + let URL = + "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_sample.html"; + + // Test that an unspecified extension is not logged by the watcher extension. + let unlogged = ExtensionTestUtils.loadExtension({ + isPrivileged: true, + manifest: { + applications: { gecko: { id: "unlogged@tests.mozilla.org" } }, + permissions: ["webRequest", "webRequestBlocking", "<all_urls>"], + }, + background() { + // This privileged test extension should not affect the webRequest + // data received by non-privileged extensions (See Bug 1576272). + browser.webRequest.onBeforeRequest.addListener( + details => { + return { cancel: false }; + }, + { urls: ["http://mochi.test/*/file_sample.html"] }, + ["blocking"] + ); + }, + }); + await unlogged.startup(); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { gecko: { id: "watched@tests.mozilla.org" } }, + permissions: [ + "tabs", + "tabHide", + "storage", + "webRequest", + "webRequestBlocking", + "<all_urls>", + ], + content_scripts: [ + { + matches: ["http://mochi.test/*/file_sample.html"], + js: ["content_script.js"], + run_at: "document_idle", + }, + ], + }, + files: { + "content_script.js": () => { + browser.test.sendMessage("content_script"); + }, + "registered_script.js": () => { + browser.test.sendMessage("registered_script"); + }, + }, + async background() { + let listen = () => {}; + async function runTest() { + // Test activity for a child function call. + browser.test.assertEq( + undefined, + browser.activityLog, + "activityLog requires permission" + ); + + // Test a child event manager. + browser.storage.onChanged.addListener(listen); + browser.storage.onChanged.removeListener(listen); + + // Test a parent event manager. + let webRequestListener = details => { + browser.webRequest.onBeforeRequest.removeListener(webRequestListener); + return { cancel: false }; + }; + browser.webRequest.onBeforeRequest.addListener( + webRequestListener, + { urls: ["http://mochi.test/*/file_sample.html"] }, + ["blocking"] + ); + + // A manifest based content script is already + // registered, we do a dynamic registration here. + await browser.contentScripts.register({ + js: [{ file: "registered_script.js" }], + matches: ["http://mochi.test/*/file_sample.html"], + runAt: "document_start", + }); + browser.test.sendMessage("ready"); + } + browser.test.onMessage.addListener((msg, data) => { + // Logging has started here so this listener is logged, but the + // call adding it was not. We do an additional onMessage.addListener + // call in the test function to validate child based event managers. + if (msg == "runtest") { + browser.test.assertTrue(true, msg); + runTest(); + } + if (msg == "hideTab") { + browser.tabs.hide(data); + } + }); + browser.test.sendMessage("url", browser.extension.getURL("")); + }, + }); + + async function backgroundScript(expectedUrl, extensionUrl) { + let expecting = [ + // Test child-only api_call. + { + type: "api_call", + name: "test.assertTrue", + data: { args: [true, "runtest"] }, + }, + + // Test child-only api_call. + { + type: "api_call", + name: "test.assertEq", + data: { + args: [undefined, undefined, "activityLog requires permission"], + }, + }, + // Test child addListener calls. + { + type: "api_call", + name: "storage.onChanged.addListener", + data: { + args: [], + }, + }, + { + type: "api_call", + name: "storage.onChanged.removeListener", + data: { + args: [], + }, + }, + // Test parent addListener calls. + { + type: "api_call", + name: "webRequest.onBeforeRequest.addListener", + data: { + args: [ + { + incognito: null, + tabId: null, + types: null, + urls: ["http://mochi.test/*/file_sample.html"], + windowId: null, + }, + ["blocking"], + ], + }, + }, + // Test an api that makes use of callParentAsyncFunction. + { + type: "api_call", + name: "contentScripts.register", + data: { + args: [ + { + allFrames: null, + css: null, + excludeGlobs: null, + excludeMatches: null, + includeGlobs: null, + js: [ + { + file: `${extensionUrl}registered_script.js`, + }, + ], + matchAboutBlank: null, + matches: ["http://mochi.test/*/file_sample.html"], + runAt: "document_start", + }, + ], + }, + }, + // Test child api_event calls. + { + type: "api_event", + name: "test.onMessage", + data: { args: ["runtest"] }, + }, + { + type: "api_call", + name: "test.sendMessage", + data: { args: ["ready"] }, + }, + // Test parent api_event calls. + { + type: "api_call", + name: "webRequest.onBeforeRequest.removeListener", + data: { + args: [], + }, + }, + { + type: "api_event", + name: "webRequest.onBeforeRequest", + data: { + args: [ + { + url: expectedUrl, + method: "GET", + type: "main_frame", + frameId: 0, + parentFrameId: -1, + incognito: false, + thirdParty: false, + ip: null, + frameAncestors: [], + urlClassification: { firstParty: [], thirdParty: [] }, + requestSize: 0, + responseSize: 0, + }, + ], + result: { + cancel: false, + }, + }, + }, + // Test manifest based content script. + { + type: "content_script", + name: "content_script.js", + data: { url: expectedUrl, tabId: 1 }, + }, + // registered script test + { + type: "content_script", + name: `${extensionUrl}registered_script.js`, + data: { url: expectedUrl, tabId: 1 }, + }, + { + type: "api_call", + name: "test.sendMessage", + data: { args: ["registered_script"], tabId: 1 }, + }, + { + type: "api_call", + name: "test.sendMessage", + data: { args: ["content_script"], tabId: 1 }, + }, + // Child api call + { + type: "api_call", + name: "tabs.hide", + data: { args: ["__TAB_ID"] }, + }, + { + type: "api_event", + name: "test.onMessage", + data: { args: ["hideTab", "__TAB_ID"] }, + }, + ]; + browser.test.assertTrue(browser.activityLog, "activityLog is privileged"); + + // Slightly less than a normal deep equal, we want to know that the values + // in our expected data are the same in the actual data, but we don't care + // if actual data has additional data or if data is in the same order in objects. + // This allows us to ignore keys that may be variable, or that are set in + // the api with an undefined value. + function deepEquivalent(a, b) { + if (a === b) { + return true; + } + if ( + typeof a != "object" || + typeof b != "object" || + a === null || + b === null + ) { + return false; + } + for (let k in a) { + if (!deepEquivalent(a[k], b[k])) { + return false; + } + } + return true; + } + + let tab; + let handler = async details => { + browser.test.log(`onExtensionActivity ${JSON.stringify(details)}`); + let test = expecting.shift(); + if (!test) { + browser.test.notifyFail(`no test for ${details.name}`); + } + + // On multiple runs, tabId will be different. Set the current + // tabId where we need it. + if (test.data.tabId !== undefined) { + test.data.tabId = tab.id; + } + if (test.data.args !== undefined) { + test.data.args = test.data.args.map(value => + value === "__TAB_ID" ? tab.id : value + ); + } + + browser.test.assertEq(test.type, details.type, "type matches"); + if (test.type == "content_script") { + browser.test.assertTrue( + details.name.includes(test.name), + "content script name matches" + ); + } else { + browser.test.assertEq(test.name, details.name, "name matches"); + } + + browser.test.assertTrue( + deepEquivalent(test.data, details.data), + `expected ${JSON.stringify( + test.data + )} included in actual ${JSON.stringify(details.data)}` + ); + if (!expecting.length) { + await browser.tabs.remove(tab.id); + browser.test.notifyPass("activity"); + } + }; + browser.activityLog.onExtensionActivity.addListener( + handler, + "watched@tests.mozilla.org" + ); + + browser.test.onMessage.addListener(async msg => { + if (msg === "opentab") { + tab = await browser.tabs.create({ url: expectedUrl }); + browser.test.sendMessage("tabid", tab.id); + } + if (msg === "done") { + browser.activityLog.onExtensionActivity.removeListener( + handler, + "watched@tests.mozilla.org" + ); + } + }); + } + + await extension.startup(); + let extensionUrl = await extension.awaitMessage("url"); + + let logger = ExtensionTestUtils.loadExtension({ + isPrivileged: true, + manifest: { + applications: { gecko: { id: "watcher@tests.mozilla.org" } }, + permissions: ["activityLog"], + }, + background: `(${backgroundScript})("${URL}", "${extensionUrl}")`, + }); + await logger.startup(); + extension.sendMessage("runtest"); + await extension.awaitMessage("ready"); + logger.sendMessage("opentab"); + let id = await logger.awaitMessage("tabid"); + + await Promise.all([ + extension.awaitMessage("content_script"), + extension.awaitMessage("registered_script"), + ]); + + extension.sendMessage("hideTab", id); + await logger.awaitFinish("activity"); + + // Stop watching because we get extra calls on extension shutdown + // such as listener removal. + logger.sendMessage("done"); + + await extension.unload(); + await unlogged.unload(); + await logger.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js b/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js new file mode 100644 index 0000000000..62933bf008 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js @@ -0,0 +1,181 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +// Tests whether not too many APIs are visible by default. +// This file is used by test_ext_all_apis.html in browser/ and mobile/android/, +// which may modify the following variables to add or remove expected APIs. +/* globals expectedContentApisTargetSpecific */ +/* globals expectedBackgroundApisTargetSpecific */ + +// Generates a list of expectations. +function generateExpectations(list) { + return list + .reduce((allApis, path) => { + return allApis.concat(`browser.${path}`, `chrome.${path}`); + }, []) + .sort(); +} + +let expectedCommonApis = [ + "extension.getURL", + "extension.inIncognitoContext", + "extension.lastError", + "i18n.detectLanguage", + "i18n.getAcceptLanguages", + "i18n.getMessage", + "i18n.getUILanguage", + "runtime.OnInstalledReason", + "runtime.OnRestartRequiredReason", + "runtime.PlatformArch", + "runtime.PlatformOs", + "runtime.RequestUpdateCheckStatus", + "runtime.getManifest", + "runtime.connect", + "runtime.getURL", + "runtime.id", + "runtime.lastError", + "runtime.onConnect", + "runtime.onMessage", + "runtime.sendMessage", + // browser.test is only available in xpcshell or when + // Cu.isInAutomation is true. + "test.assertEq", + "test.assertFalse", + "test.assertRejects", + "test.assertThrows", + "test.assertTrue", + "test.fail", + "test.log", + "test.notifyFail", + "test.notifyPass", + "test.onMessage", + "test.sendMessage", + "test.succeed", + "test.withHandlingUserInput", +]; + +let expectedContentApis = [ + ...expectedCommonApis, + ...expectedContentApisTargetSpecific, +]; + +let expectedBackgroundApis = [ + ...expectedCommonApis, + ...expectedBackgroundApisTargetSpecific, + "contentScripts.register", + "experiments.APIChildScope", + "experiments.APIEvent", + "experiments.APIParentScope", + "extension.ViewType", + "extension.getBackgroundPage", + "extension.getViews", + "extension.isAllowedFileSchemeAccess", + "extension.isAllowedIncognitoAccess", + // Note: extensionTypes is not visible in Chrome. + "extensionTypes.CSSOrigin", + "extensionTypes.ImageFormat", + "extensionTypes.RunAt", + "management.ExtensionDisabledReason", + "management.ExtensionInstallType", + "management.ExtensionType", + "management.getSelf", + "management.uninstallSelf", + "permissions.getAll", + "permissions.contains", + "permissions.request", + "permissions.remove", + "permissions.onAdded", + "permissions.onRemoved", + "runtime.getBackgroundPage", + "runtime.getBrowserInfo", + "runtime.getPlatformInfo", + "runtime.onConnectExternal", + "runtime.onInstalled", + "runtime.onMessageExternal", + "runtime.onStartup", + "runtime.onUpdateAvailable", + "runtime.openOptionsPage", + "runtime.reload", + "runtime.setUninstallURL", + "theme.getCurrent", + "theme.onUpdated", + "types.LevelOfControl", + "types.SettingScope", +]; + +function sendAllApis() { + function isEvent(key, val) { + if (!/^on[A-Z]/.test(key)) { + return false; + } + let eventKeys = []; + for (let prop in val) { + eventKeys.push(prop); + } + eventKeys = eventKeys.sort().join(); + return eventKeys === "addListener,hasListener,removeListener"; + } + function mayRecurse(key, val) { + if (Object.keys(val).filter(k => !/^[A-Z\-0-9_]+$/.test(k)).length === 0) { + // Don't recurse on constants and empty objects. + return false; + } + return !isEvent(key, val); + } + + let results = []; + function diveDeeper(path, obj) { + for (let key in obj) { + let val = obj[key]; + if (typeof val == "object" && val !== null && mayRecurse(key, val)) { + diveDeeper(`${path}.${key}`, val); + } else if (val !== undefined) { + results.push(`${path}.${key}`); + } + } + } + diveDeeper("browser", browser); + diveDeeper("chrome", chrome); + browser.test.sendMessage("allApis", results.sort()); +} + +add_task(async function test_enumerate_content_script_apis() { + let extensionData = { + manifest: { + content_scripts: [ + { + matches: ["http://mochi.test/*/file_sample.html"], + js: ["contentscript.js"], + run_at: "document_start", + }, + ], + }, + files: { + "contentscript.js": sendAllApis, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let win = window.open("file_sample.html"); + let actualApis = await extension.awaitMessage("allApis"); + win.close(); + let expectedApis = generateExpectations(expectedContentApis); + isDeeply(actualApis, expectedApis, "content script APIs"); + + await extension.unload(); +}); + +add_task(async function test_enumerate_background_script_apis() { + let extensionData = { + background: sendAllApis, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let actualApis = await extension.awaitMessage("allApis"); + let expectedApis = generateExpectations(expectedBackgroundApis); + isDeeply(actualApis, expectedApis, "background script APIs"); + + await extension.unload(); +}); diff --git a/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html b/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html new file mode 100644 index 0000000000..ffa421e042 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html @@ -0,0 +1,376 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Async Clipboard permissions tests</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> + +<script> +"use strict"; + +// Bug 1479956 - On android-debug verify this test times out +SimpleTest.requestLongerTimeout(2); + +/* globals clipboardWriteText, clipboardWrite, clipboardReadText, clipboardRead */ +function shared() { + this.clipboardWriteText = function(txt) { + return navigator.clipboard.writeText(txt); + }; + + this.clipboardWrite = function(dt) { + return navigator.clipboard.write(dt); + }; + + this.clipboardReadText = function() { + return navigator.clipboard.readText(); + }; + + this.clipboardRead = function() { + return navigator.clipboard.read(); + }; +} + +/** + * Clear the clipboard. + * + * This is needed because Services.clipboard.emptyClipboard() does not clear the actual system clipboard. + */ +function clearClipboard() { + if (AppConstants.platform == "android") { + // On android, this clears the actual system clipboard + SpecialPowers.Services.clipboard.emptyClipboard(SpecialPowers.Services.clipboard.kGlobalClipboard); + return; + } + // Need to do this hack on other platforms to clear the actual system clipboard + let transf = SpecialPowers.Cc["@mozilla.org/widget/transferable;1"] + .createInstance(SpecialPowers.Ci.nsITransferable); + transf.init(null); + // Empty transferables may cause crashes, so just add an unknown type. + const TYPE = "text/x-moz-place-empty"; + transf.addDataFlavor(TYPE); + transf.setTransferData(TYPE, {}, 0); + SpecialPowers.Services.clipboard.setData(transf, null, SpecialPowers.Services.clipboard.kGlobalClipboard); +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.events.asyncClipboard", true], + ["dom.events.asyncClipboard.dataTransfer", true], + ]}); +}); + +// Test that without enough permissions, we are NOT allowed to use writeText, write, read or readText in background script +add_task(async function test_background_async_clipboard_no_permissions() { + function backgroundScript() { + let dt = new DataTransfer(); + dt.items.add("Howdy", "text/plain"); + browser.test.assertRejects(clipboardRead(), undefined, "Read should be denied without permission"); + browser.test.assertRejects(clipboardWrite(dt), undefined, "Write should be denied without permission"); + browser.test.assertRejects(clipboardWriteText("blabla"), undefined, "WriteText should be denied without permission"); + browser.test.assertRejects(clipboardReadText(), undefined, "ReadText should be denied without permission"); + browser.test.sendMessage("ready"); + } + let extensionData = { + background: [shared, backgroundScript], + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await extension.awaitMessage("ready"); + await extension.unload(); +}); + +// Test that without enough permissions, we are NOT allowed to use writeText, write, read or readText in content script +add_task(async function test_contentscript_async_clipboard_no_permission() { + function contentScript() { + let dt = new DataTransfer(); + dt.items.add("Howdy", "text/plain"); + browser.test.assertRejects(clipboardRead(), undefined, "Read should be denied without permission"); + browser.test.assertRejects(clipboardWrite(dt), undefined, "Write should be denied without permission"); + browser.test.assertRejects(clipboardWriteText("blabla"), undefined, "WriteText should be denied without permission"); + browser.test.assertRejects(clipboardReadText(), undefined, "ReadText should be denied without permission"); + browser.test.sendMessage("ready"); + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["https://example.com/*/file_sample.html"], + }], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + await extension.awaitMessage("ready"); + win.close(); + await extension.unload(); +}); + +// Test that with enough permissions, we are allowed to use writeText in content script +add_task(async function test_contentscript_clipboard_permission_writetext() { + function contentScript() { + let str = "HI"; + clipboardWriteText(str).then(function() { + // nothing here + browser.test.sendMessage("ready"); + }, function(err) { + browser.test.fail("WriteText promise rejected"); + browser.test.sendMessage("ready"); + }); // clipboardWriteText + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["https://example.com/*/file_sample.html"], + }], + permissions: [ + "clipboardWrite", + "clipboardRead", + ], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + await extension.awaitMessage("ready"); + const actual = SpecialPowers.getClipboardData("text/unicode"); + is(actual, "HI", "right string copied by write"); + win.close(); + await extension.unload(); +}); + +// Test that with enough permissions, we are allowed to use readText in content script +add_task(async function test_contentscript_clipboard_permission_readtext() { + function contentScript() { + let str = "HI"; + clipboardReadText().then(function(strData) { + if (strData == str) { + browser.test.succeed("Successfully read from clipboard"); + } else { + browser.test.fail("ReadText read the wrong thing from clipboard:" + strData); + } + browser.test.sendMessage("ready"); + }, function(err) { + browser.test.fail("ReadText promise rejected"); + browser.test.sendMessage("ready"); + }); // clipboardReadText + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["https://example.com/*/file_sample.html"], + }], + permissions: [ + "clipboardWrite", + "clipboardRead", + ], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + await SimpleTest.promiseClipboardChange("HI", () => { + SpecialPowers.clipboardCopyString("HI"); + }, "text/unicode"); + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + await extension.awaitMessage("ready"); + win.close(); + await extension.unload(); +}); + +// Test that with enough permissions, we are allowed to use write in content script +add_task(async function test_contentscript_clipboard_permission_write() { + function contentScript() { + let str = "HI"; + let dt = new DataTransfer(); + dt.items.add(str, "text/plain"); + clipboardWrite(dt).then(function() { + // nothing here + browser.test.sendMessage("ready"); + }, function(err) { // clipboardWrite promise error function + browser.test.fail("Write promise rejected"); + browser.test.sendMessage("ready"); + }); // clipboard write + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["https://example.com/*/file_sample.html"], + }], + permissions: [ + "clipboardWrite", + "clipboardRead", + ], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + await extension.awaitMessage("ready"); + const actual = SpecialPowers.getClipboardData("text/unicode"); + is(actual, "HI", "right string copied by write"); + win.close(); + await extension.unload(); +}); + +// Test that with enough permissions, we are allowed to use read in content script +add_task(async function test_contentscript_clipboard_permission_read() { + function contentScript() { + clipboardRead().then(function(dt) { + let s = dt.getData("text/plain"); + if (s == "HELLO") { + browser.test.succeed("Read promise successfully read the right thing"); + } else { + browser.test.fail("Read read the wrong string from clipboard:" + s); + } + browser.test.sendMessage("ready"); + }, function(err) { // clipboardRead promise error function + browser.test.fail("Read promise rejected"); + browser.test.sendMessage("ready"); + }); // clipboard read + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["https://example.com/*/file_sample.html"], + }], + permissions: [ + "clipboardWrite", + "clipboardRead", + ], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + await SimpleTest.promiseClipboardChange("HELLO", () => { + SpecialPowers.clipboardCopyString("HELLO"); + }, "text/unicode"); + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + await extension.awaitMessage("ready"); + win.close(); + await extension.unload(); +}); + +// Test that performing readText(...) when the clipboard is empty returns an empty string +add_task(async function test_contentscript_clipboard_nocontents_readtext() { + function contentScript() { + clipboardReadText().then(function(strData) { + if (strData == "") { + browser.test.succeed("ReadText successfully read correct thing from an empty clipboard"); + } else { + browser.test.fail("ReadText should have read an empty string, but read:" + strData); + } + browser.test.sendMessage("ready"); + }, function(err) { + browser.test.fail("ReadText promise rejected: " + err); + browser.test.sendMessage("ready"); + }); + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["https://example.com/*/file_sample.html"], + }], + permissions: [ + "clipboardRead", + ], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + + await SimpleTest.promiseClipboardChange("", () => { + clearClipboard(); + }, "text/x-moz-place-empty"); + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + await extension.awaitMessage("ready"); + win.close(); + await extension.unload(); +}); + +// Test that performing read(...) when the clipboard is empty returns an empty data transfer +add_task(async function test_contentscript_clipboard_nocontents_read() { + function contentScript() { + clipboardRead().then(function(dataT) { + // On macOS if we clear the clipboard and read from it, there will be + // no items in the data transfer object. + // On linux with e10s enabled clearing of the clipboard does not happen in + // the same way as it does on other platforms. So when we clear the clipboard + // and read from it, the data transfer object contains an item of type + // text/plain and kind string, but we can't call getAsString on it to verify + // that at least it is an empty string because the callback never gets invoked. + if (!dataT.items.length || + (dataT.items.length == 1 && dataT.items[0].type == "text/plain" && + dataT.items[0].kind == "string")) { + browser.test.succeed("Read promise successfully resolved"); + } else { + browser.test.fail("Read read the wrong thing from clipboard, " + + "data transfer has this many items:" + dataT.items.length); + } + browser.test.sendMessage("ready"); + }, function(err) { + browser.test.fail("Read promise rejected: " + err); + browser.test.sendMessage("ready"); + }); + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["https://example.com/*/file_sample.html"], + }], + permissions: [ + "clipboardRead", + ], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + + await SimpleTest.promiseClipboardChange("", () => { + clearClipboard(); + }, "text/x-moz-place-empty"); + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + await extension.awaitMessage("ready"); + win.close(); + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html b/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html new file mode 100644 index 0000000000..8b6fba25bb --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for background page canvas rendering</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_background_canvas() { + function background() { + try { + let canvas = document.createElement("canvas"); + + let context = canvas.getContext("2d"); + + // This ensures that we have a working PresShell, and can successfully + // calculate font metrics. + context.font = "8pt fixed"; + + browser.test.notifyPass("background-canvas"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("background-canvas"); + } + } + + let extensionData = { + useAddonManager: "permanent", + manifest: { + applications: { gecko: { id: "background_canvas@tests.mozilla.org" } }, + }, + background, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + await extension.awaitFinish("background-canvas"); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_page.html b/toolkit/components/extensions/test/mochitest/test_ext_background_page.html new file mode 100644 index 0000000000..9cafd8a61a --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_background_page.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>WebExtension test</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js" type="text/javascript"></script> + <link href="/tests/SimpleTest/test.css" rel="stylesheet"/> + </head> + <body> + + <script type="text/javascript"> + "use strict"; + + /* eslint-disable mozilla/balanced-listeners */ + + add_task(async function testAlertNotShownInBackgroundWindow() { + let extension = ExtensionTestUtils.loadExtension({ + background: function () { + alert("I am an alert in the background."); + + browser.test.notifyPass("alertCalled"); + } + }); + + let consoleOpened = loadChromeScript(() => { + const {sendAsyncMessage, assert} = this; + assert.ok(!Services.wm.getEnumerator("alert:alert").hasMoreElements(), "Alerts should not be present at the start of the test."); + + Services.obs.addObserver(function observer() { + sendAsyncMessage("web-console-created"); + Services.obs.removeObserver(observer, "web-console-created"); + }, "web-console-created"); + }); + let opened = consoleOpened.promiseOneMessage("web-console-created"); + + consoleMonitor.start([ + { + message: /alert\(\) is not supported in background windows/ + }, { + message: /I am an alert in the background/ + } + ]); + + await extension.startup(); + await extension.awaitFinish("alertCalled"); + + let chromeScript = loadChromeScript(async () => { + const {assert} = this; + assert.ok(!Services.wm.getEnumerator("alert:alert").hasMoreElements(), "Alerts should not be present after calling alert()."); + }); + chromeScript.destroy(); + + await consoleMonitor.finished(); + + await opened; + consoleOpened.destroy(); + + chromeScript = loadChromeScript(async () => { + const {sendAsyncMessage} = this; + let {require} = ChromeUtils.import ("resource://devtools/shared/Loader.jsm"); + require("devtools/client/framework/devtools-browser"); + let {BrowserConsoleManager} = require("devtools/client/webconsole/browser-console-manager"); + + // And then double check that we have an actual browser console. + let haveConsole = !!BrowserConsoleManager.getBrowserConsole(); + + if (haveConsole) { + await BrowserConsoleManager.toggleBrowserConsole(); + } + sendAsyncMessage("done", haveConsole); + }); + + let consoleShown = await chromeScript.promiseOneMessage("done"); + ok(consoleShown, "console was shown"); + chromeScript.destroy(); + + await extension.unload(); + }); + </script> + + </body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_indexedDB.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_indexedDB.html new file mode 100644 index 0000000000..f7d36633db --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_indexedDB.html @@ -0,0 +1,161 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Test browsingData.remove indexedDB</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function testIndexedDB() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.userContext.enabled", true]], + }); + + async function background() { + const PAGE = + "/tests/toolkit/components/extensions/test/mochitest/file_indexedDB.html"; + + let tabs = []; + + browser.test.onMessage.addListener(async msg => { + if (msg == "cleanup") { + await Promise.all(tabs.map(tabId => browser.tabs.remove(tabId))); + browser.test.sendMessage("done"); + return; + } + + await browser.browsingData.remove(msg, { indexedDB: true }); + browser.test.sendMessage("indexedDBRemoved"); + }); + + // Create two tabs. + let tab = await browser.tabs.create({ url: `http://mochi.test:8888${PAGE}` }); + tabs.push(tab.id); + + tab = await browser.tabs.create({ url: `http://example.com${PAGE}` }); + tabs.push(tab.id); + + // Create tab with cookieStoreId "firefox-container-1" + tab = await browser.tabs.create({ url: `http://example.net${PAGE}`, cookieStoreId: 'firefox-container-1' }); + tabs.push(tab.id); + } + + function contentScript() { + // eslint-disable-next-line mozilla/balanced-listeners + window.addEventListener( + "message", + msg => { + browser.test.sendMessage("indexedDBCreated"); + }, + true + ); + } + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + background, + manifest: { + applications: { gecko: { id: "indexedDb@tests.mozilla.org" } }, + permissions: ["browsingData", "tabs", "cookies"], + content_scripts: [ + { + matches: [ + "http://mochi.test/*/file_indexedDB.html", + "http://example.com/*/file_indexedDB.html", + "http://example.net/*/file_indexedDB.html", + ], + js: ["script.js"], + run_at: "document_start", + }, + ], + }, + files: { + "script.js": contentScript, + }, + }); + + await extension.startup(); + + await extension.awaitMessage("indexedDBCreated"); + await extension.awaitMessage("indexedDBCreated"); + await extension.awaitMessage("indexedDBCreated"); + + function getUsage() { + return new Promise(resolve => { + let qms = SpecialPowers.Services.qms; + let cb = SpecialPowers.wrapCallback(request => resolve(request.result)); + qms.getUsage(cb); + }); + } + + async function getOrigins() { + let origins = []; + let result = await getUsage(); + for (let i = 0; i < result.length; ++i) { + if (result[i].usage === 0) { + continue; + } + if ( + result[i].origin.startsWith("http://mochi.test") || + result[i].origin.startsWith("http://example.com") || + result[i].origin.startsWith("http://example.net") + ) { + origins.push(result[i].origin); + } + } + return origins.sort(); + } + + let origins = await getOrigins(); + is(origins.length, 3, "IndexedDB databases have been populated."); + + // Deleting private browsing mode data is silently ignored. + extension.sendMessage({ cookieStoreId: "firefox-private" }); + await extension.awaitMessage("indexedDBRemoved"); + + origins = await getOrigins(); + is(origins.length, 3, "All indexedDB remains after clearing firefox-private"); + + // Delete by hostname + extension.sendMessage({ hostnames: ["example.com"] }); + await extension.awaitMessage("indexedDBRemoved"); + + origins = await getOrigins(); + is(origins.length, 2, "IndexedDB data only for only two domains left"); + ok(origins[0].startsWith("http://example.net"), "example.net not deleted"); + ok(origins[1].startsWith("http://mochi.test"), "mochi.test not deleted"); + + // TODO: Bug 1643740 + if (AppConstants.platform != "android") { + // Delete by cookieStoreId + extension.sendMessage({ cookieStoreId: "firefox-container-1" }); + await extension.awaitMessage("indexedDBRemoved"); + + origins = await getOrigins(); + is(origins.length, 1, "IndexedDB data only for only one domain"); + ok(origins[0].startsWith("http://mochi.test"), "mochi.test not deleted"); + } + + // Delete all + extension.sendMessage({}); + await extension.awaitMessage("indexedDBRemoved"); + + origins = await getOrigins(); + is(origins.length, 0, "All IndexedDB data has been removed."); + + await extension.sendMessage("cleanup"); + await extension.awaitMessage("done"); + + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_localStorage.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_localStorage.html new file mode 100644 index 0000000000..cf6c420366 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_localStorage.html @@ -0,0 +1,322 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Test browsingData.remove indexedDB</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function setup() { + // make sure userContext is enabled. + return SpecialPowers.pushPrefEnv({ + set: [["privacy.userContext.enabled", true]], + }); +}); + +add_task(async function testLocalStorage() { + async function background() { + function waitForTabs() { + return new Promise(resolve => { + let tabs = {}; + + let listener = async (msg, { tab }) => { + if (msg !== "content-script-ready") { + return; + } + + tabs[tab.url] = tab; + if (Object.keys(tabs).length == 3) { + browser.runtime.onMessage.removeListener(listener); + resolve(tabs); + } + }; + browser.runtime.onMessage.addListener(listener); + }); + } + + function sendMessageToTabs(tabs, message) { + return Promise.all( + Object.values(tabs).map(tab => { + return browser.tabs.sendMessage(tab.id, message); + }) + ); + } + + let tabs = await waitForTabs(); + + browser.test.assertRejects( + browser.browsingData.removeLocalStorage({ since: Date.now() }), + "Firefox does not support clearing localStorage with 'since'.", + "Expected error received when using unimplemented parameter 'since'." + ); + + await sendMessageToTabs(tabs, "resetLocalStorage"); + await browser.browsingData.removeLocalStorage({ + hostnames: ["example.com"], + }); + await browser.tabs.sendMessage(tabs["http://example.com/"].id, "checkLocalStorageCleared"); + await browser.tabs.sendMessage(tabs["http://example.net/"].id, "checkLocalStorageSet"); + + if ( + SpecialPowers.Services.domStorageManager.nextGenLocalStorageEnabled === + false + ) { + // This assertion fails when localStorage is using the legacy + // implementation (See Bug 1595431). + browser.test.log("Skipped assertion on nextGenLocalStorageEnabled=false"); + } else { + await browser.tabs.sendMessage(tabs["http://test1.example.com/"].id, "checkLocalStorageSet"); + } + + await sendMessageToTabs(tabs, "resetLocalStorage"); + await sendMessageToTabs(tabs, "checkLocalStorageSet"); + await browser.browsingData.removeLocalStorage({}); + await sendMessageToTabs(tabs, "checkLocalStorageCleared"); + + await sendMessageToTabs(tabs, "resetLocalStorage"); + await sendMessageToTabs(tabs, "checkLocalStorageSet"); + await browser.browsingData.remove({}, { localStorage: true }); + await sendMessageToTabs(tabs, "checkLocalStorageCleared"); + + // Can only delete cookieStoreId with LSNG enabled. + if (SpecialPowers.Services.domStorageManager.nextGenLocalStorageEnabled) { + await sendMessageToTabs(tabs, "resetLocalStorage"); + await sendMessageToTabs(tabs, "checkLocalStorageSet"); + await browser.browsingData.removeLocalStorage({ + cookieStoreId: "firefox-container-1", + }); + await browser.tabs.sendMessage(tabs["http://example.com/"].id, "checkLocalStorageSet"); + await browser.tabs.sendMessage(tabs["http://example.net/"].id, "checkLocalStorageSet"); + + // TODO: containers support is lacking on GeckoView (Bug 1643740) + if (!navigator.userAgent.includes("Android")) { + await browser.tabs.sendMessage(tabs["http://test1.example.com/"].id, "checkLocalStorageCleared"); + } + + await sendMessageToTabs(tabs, "resetLocalStorage"); + await sendMessageToTabs(tabs, "checkLocalStorageSet"); + // Hostname doesn't match, so nothing cleared. + await browser.browsingData.removeLocalStorage({ + cookieStoreId: "firefox-container-1", + hostnames: ["example.net"], + }); + await sendMessageToTabs(tabs, "checkLocalStorageSet"); + + await sendMessageToTabs(tabs, "resetLocalStorage"); + await sendMessageToTabs(tabs, "checkLocalStorageSet"); + // Deleting private browsing mode data is silently ignored. + await browser.browsingData.removeLocalStorage({ + cookieStoreId: "firefox-private", + }); + await sendMessageToTabs(tabs, "checkLocalStorageSet"); + } else { + await browser.test.assertRejects( + browser.browsingData.removeLocalStorage({ + cookieStoreId: "firefox-container-1", + }), + "removeLocalStorage with cookieStoreId requires LSNG" + ); + } + + // Cleanup (checkLocalStorageCleared creates empty LS databases). + await browser.browsingData.removeLocalStorage({}); + + browser.test.notifyPass("done"); + } + + function contentScript() { + browser.runtime.onMessage.addListener(msg => { + if (msg === "resetLocalStorage") { + localStorage.clear(); + localStorage.setItem("test", "test"); + } else if (msg === "checkLocalStorageSet") { + browser.test.assertEq( + "test", + localStorage.getItem("test"), + `checkLocalStorageSet: ${location.href}` + ); + } else if (msg === "checkLocalStorageCleared") { + browser.test.assertEq( + null, + localStorage.getItem("test"), + `checkLocalStorageCleared: ${location.href}` + ); + } + }); + browser.runtime.sendMessage("content-script-ready"); + } + + // This extension is responsible for opening tabs with a specified + // cookieStoreId, we use a separate extension to make sure that browsingData + // works without the cookies permission. + let openTabsExtension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + name: "Open tabs", + applications: { gecko: { id: "open-tabs@tests.mozilla.org" }, }, + permissions: ["cookies"], + }, + async background() { + const TABS = [ + { url: "http://example.com" }, + { url: "http://example.net" }, + { + url: "http://test1.example.com", + cookieStoreId: 'firefox-container-1', + }, + ]; + + function awaitLoad(tabId) { + return new Promise(resolve => { + browser.tabs.onUpdated.addListener(function listener(tabId_, changed, tab) { + if (tabId == tabId_ && changed.status == "complete") { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + } + + let tabs = []; + let loaded = []; + for (let options of TABS) { + let tab = await browser.tabs.create(options); + loaded.push(awaitLoad(tab.id)); + tabs.push(tab); + } + + await Promise.all(loaded); + + browser.test.onMessage.addListener(async msg => { + if (msg === "cleanup") { + const tabIds = tabs.map(tab => tab.id); + let removedTabs = 0; + browser.tabs.onRemoved.addListener(tabId => { + browser.test.log(`Removing tab ${tabId}.`); + if (tabIds.includes(tabId)) { + removedTabs++; + if (removedTabs == tabIds.length) { + browser.test.sendMessage("done"); + } + } + }); + await browser.tabs.remove(tabIds); + } + }); + } + }); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + background, + manifest: { + name: "Test Extension", + applications: { gecko: { id: "localStorage@tests.mozilla.org" } }, + permissions: ["browsingData", "tabs"], + content_scripts: [ + { + matches: [ + "http://example.com/", + "http://example.net/", + "http://test1.example.com/", + ], + js: ["content-script.js"], + run_at: "document_end", + }, + ], + }, + files: { + "content-script.js": contentScript, + }, + }); + + await openTabsExtension.startup(); + + await extension.startup(); + await extension.awaitFinish("done"); + await extension.unload(); + + await openTabsExtension.sendMessage("cleanup"); + await openTabsExtension.awaitMessage("done"); + await openTabsExtension.unload(); +}); + +// Verify that browsingData.removeLocalStorage doesn't break on data stored +// in about:newtab or file principals. +add_task(async function test_browserData_on_aboutnewtab_and_file_data() { + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + async background() { + await browser.browsingData.removeLocalStorage({}).catch(err => { + browser.test.fail(`${err} :: ${err.stack}`); + }); + browser.test.sendMessage("done"); + }, + manifest: { + applications: { gecko: { id: "indexed-db-file@test.mozilla.org" } }, + permissions: ["browsingData"], + }, + }); + + await new Promise(resolve => { + const chromeScript = SpecialPowers.loadChromeScript(async () => { + const { SiteDataTestUtils } = ChromeUtils.import( + "resource://testing-common/SiteDataTestUtils.jsm" + ); + await SiteDataTestUtils.addToIndexedDB("about:newtab"); + await SiteDataTestUtils.addToIndexedDB("file:///fake/file"); + // eslint-disable-next-line no-undef + sendAsyncMessage("done"); + }); + + chromeScript.addMessageListener("done", () => { + chromeScript.destroy(); + resolve(); + }); + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function test_browserData_should_not_remove_extension_data() { + if (!SpecialPowers.getBoolPref("dom.storage.next_gen")) { + // When LSNG isn't enabled, the browsingData API does still clear + // all the extensions localStorage if called without a list of specific + // origins to clear. + info("Test skipped because LSNG is currently disabled"); + return; + } + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + async background() { + window.localStorage.setItem("key", "value"); + await browser.browsingData.removeLocalStorage({}).catch(err => { + browser.test.fail(`${err} :: ${err.stack}`); + }); + browser.test.sendMessage("done", window.localStorage.getItem("key")); + }, + manifest: { + applications: { gecko: { id: "extension-data@tests.mozilla.org" } }, + permissions: ["browsingData"], + }, + }); + + await extension.startup(); + const lsValue = await extension.awaitMessage("done"); + is(lsValue, "value", "Got the expected localStorage data"); + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_pluginData.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_pluginData.html new file mode 100644 index 0000000000..ff75ca7b9f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_pluginData.html @@ -0,0 +1,71 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Test browsingData.remove indexedDB</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +// NB: Since plugins are disabled, there is never any data to clear. +// We are really testing that these operations are no-ops. + +add_task(async function testPluginData() { + async function background() { + const REFERENCE_DATE = Date.now(); + const TEST_CASES = [ + // Clear plugin data with no since value. + {}, + // Clear pluginData with recent since value. + { since: REFERENCE_DATE - 20000 }, + // Clear pluginData with old since value. + { since: REFERENCE_DATE - 1000000 }, + // Clear pluginData for specific hosts. + { hostnames: ["bar.com", "baz.com"] }, + // Clear pluginData for no hosts. + { hostnames: [] }, + ]; + + for (let method of ["removePluginData", "remove"]) { + for (let options of TEST_CASES) { + browser.test.log(`Testing ${method} with ${JSON.stringify(options)}`); + if (method == "removePluginData") { + await browser.browsingData.removePluginData(options); + } else { + await browser.browsingData.remove(options, { pluginData: true }); + } + } + } + + browser.test.sendMessage("done"); + } + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + background, + manifest: { + applications: { gecko: { id: "remove-plugin@tests.mozilla.org" } }, + permissions: ["tabs", "browsingData"], + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + + // This test has no assertions because it's only meant to check that we don't + // throw when calling removePluginData and remove with pluginData: true. + ok(true, "dummy check"); + + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_serviceWorkers.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_serviceWorkers.html new file mode 100644 index 0000000000..a97a62a0f4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_serviceWorkers.html @@ -0,0 +1,141 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Test browsingData.remove indexedDB</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const { TestUtils } = SpecialPowers.Cu.import("resource://testing-common/TestUtils.jsm"); + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); +}); + +add_task(async function testServiceWorkers() { + async function background() { + const PAGE = + "/tests/toolkit/components/extensions/test/mochitest/file_serviceWorker.html"; + + browser.runtime.onMessage.addListener(msg => { + browser.test.sendMessage("serviceWorkerRegistered"); + }); + + let tabs = []; + + browser.test.onMessage.addListener(async msg => { + if (msg == "cleanup") { + await browser.tabs.remove(tabs.map(tab => tab.id)); + browser.test.sendMessage("done"); + return; + } + + await browser.browsingData.remove( + { hostnames: msg.hostnames }, + { serviceWorkers: true } + ); + browser.test.sendMessage("serviceWorkersRemoved"); + }); + + // Create two serviceWorkers. + let tab = await browser.tabs.create({ url: `http://mochi.test:8888${PAGE}` }); + tabs.push(tab); + + tab = await browser.tabs.create({ url: `http://example.com${PAGE}` }); + tabs.push(tab); + } + + function contentScript() { + // eslint-disable-next-line mozilla/balanced-listeners + window.addEventListener( + "message", + msg => { + if (msg.data == "serviceWorkerRegistered") { + browser.runtime.sendMessage("serviceWorkerRegistered"); + } + }, + true + ); + } + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + background, + manifest: { + applications: { gecko: { id: "service-workers@tests.mozilla.org" } }, + permissions: ["browsingData", "tabs"], + content_scripts: [ + { + matches: [ + "http://mochi.test/*/file_serviceWorker.html", + "http://example.com/*/file_serviceWorker.html", + ], + js: ["script.js"], + run_at: "document_start", + }, + ], + }, + files: { + "script.js": contentScript, + }, + }); + + await extension.startup(); + await extension.awaitMessage("serviceWorkerRegistered"); + await extension.awaitMessage("serviceWorkerRegistered"); + + // Even though we await the registrations by waiting for the messages, + // sometimes the serviceWorkers are still not registered at this point. + async function getRegistrations(count) { + await TestUtils.waitForCondition( + async () => (await SpecialPowers.registeredServiceWorkers()).length === count, + `Wait for ${count} service workers to be registered` + ); + return SpecialPowers.registeredServiceWorkers(); + } + + let serviceWorkers = await getRegistrations(2); + is(serviceWorkers.length, 2, "ServiceWorkers have been registered."); + + extension.sendMessage({ hostnames: ["example.com"] }); + await extension.awaitMessage("serviceWorkersRemoved"); + + serviceWorkers = await getRegistrations(1); + is( + serviceWorkers.length, + 1, + "ServiceWorkers for example.com have been removed." + ); + + let { scriptSpec } = serviceWorkers[0]; + dump(`Service worker spec: ${scriptSpec}`); + ok(scriptSpec.startsWith("http://mochi.test:8888/"), + "ServiceWorkers for example.com have been removed."); + + extension.sendMessage({}); + await extension.awaitMessage("serviceWorkersRemoved"); + + serviceWorkers = await getRegistrations(0); + is(serviceWorkers.length, 0, "All ServiceWorkers have been removed."); + + extension.sendMessage("cleanup"); + await extension.awaitMessage("done"); + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_settings.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_settings.html new file mode 100644 index 0000000000..3b1d5e1af9 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_settings.html @@ -0,0 +1,67 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Test browsingData.settings</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const SETTINGS_LIST = [ + "cache", + "cookies", + "history", + "formData", + "downloads", +].sort(); + +add_task(async function testSettings() { + async function background() { + browser.browsingData.settings().then(settings => { + browser.test.sendMessage("settings", settings); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + background, + manifest: { + applications: { gecko: { id: "browsingData-settings@tests.mozilla.org" } }, + permissions: ["browsingData"], + }, + }); + + await extension.startup(); + let settings = await extension.awaitMessage("settings"); + + // Verify that we get the keys back we expect. + isDeeply( + Object.entries(settings.dataToRemove) + .filter(([key, value]) => value) + .map(([key, value]) => key) + .sort(), + SETTINGS_LIST, + "dataToRemove contains expected properties." + ); + isDeeply( + Object.entries(settings.dataRemovalPermitted) + .filter(([key, value]) => value) + .map(([key, value]) => key) + .sort(), + SETTINGS_LIST, + "dataToRemove contains expected properties." + ); + is("since" in settings.options, true, "options contains |since|"); + + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_canvas_resistFingerprinting.html b/toolkit/components/extensions/test/mochitest/test_ext_canvas_resistFingerprinting.html new file mode 100644 index 0000000000..7116d03235 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_canvas_resistFingerprinting.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <script type="text/javascript" src="head_cookies.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting", true]], + }); +}); + +add_task(async function test_contentscript() { + function contentScript() { + let canvas = document.createElement("canvas"); + canvas.width = canvas.height = "100"; + + let ctx = canvas.getContext("2d"); + ctx.fillStyle = "green"; + ctx.fillRect(0, 0, 100, 100); + let data = ctx.getImageData(0, 0, 100, 100); + + browser.test.sendMessage("data-color", data.data[1]); + } + + let extensionData = { + manifest: { + content_scripts: [ + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_start", + }, + ], + }, + + files: { + "content_script.js": contentScript, + }, + }; + const url = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest/file_sample.html"; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + let win = window.open(url); + let color = await extension.awaitMessage("data-color"); + is(color, 128, "Got correct pixel data for green"); + win.close(); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_clipboard.html b/toolkit/components/extensions/test/mochitest/test_ext_clipboard.html new file mode 100644 index 0000000000..77ac767391 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_clipboard.html @@ -0,0 +1,210 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Clipboard permissions tests</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> + +<script> +"use strict"; + +/* globals doCopy, doPaste */ +function shared() { + let field = document.createElement("textarea"); + document.body.appendChild(field); + field.contentEditable = true; + + this.doCopy = function(txt) { + field.value = txt; + field.select(); + return document.execCommand("copy"); + }; + + this.doPaste = function() { + field.select(); + return document.execCommand("paste") && field.value; + }; +} + +add_task(async function test_background_clipboard_permissions() { + function backgroundScript() { + browser.test.assertEq(false, doCopy("whatever"), + "copy should be denied without permission"); + browser.test.assertEq(false, doPaste(), + "paste should be denied without permission"); + browser.test.sendMessage("ready"); + } + let extensionData = { + background: [shared, backgroundScript], + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + await extension.awaitMessage("ready"); + + await extension.unload(); +}); + +add_task(async function test_background_clipboard_copy() { + function backgroundScript() { + browser.test.onMessage.addListener(txt => { + browser.test.assertEq(true, doCopy(txt), + "copy should be allowed with permission"); + }); + browser.test.sendMessage("ready"); + } + let extensionData = { + background: `(${shared})();(${backgroundScript})();`, + manifest: { + permissions: [ + "clipboardWrite", + ], + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await extension.awaitMessage("ready"); + + const DUMMY_STR = "dummy string to copy"; + await new Promise(resolve => { + SimpleTest.waitForClipboard(DUMMY_STR, () => { + extension.sendMessage(DUMMY_STR); + }, resolve, resolve); + }); + + await extension.unload(); +}); + +add_task(async function test_contentscript_clipboard_permissions() { + function contentScript() { + browser.test.assertEq(false, doCopy("whatever"), + "copy should be denied without permission"); + browser.test.assertEq(false, doPaste(), + "paste should be denied without permission"); + browser.test.sendMessage("ready"); + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["http://mochi.test/*/file_sample.html"], + }], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let win = window.open("file_sample.html"); + await extension.awaitMessage("ready"); + win.close(); + + await extension.unload(); +}); + +add_task(async function test_contentscript_clipboard_copy() { + function contentScript() { + browser.test.onMessage.addListener(txt => { + browser.test.assertEq(true, doCopy(txt), + "copy should be allowed with permission"); + }); + browser.test.sendMessage("ready"); + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["shared.js", "contentscript.js"], + matches: ["http://mochi.test/*/file_sample.html"], + }], + permissions: [ + "clipboardWrite", + ], + }, + files: { + "shared.js": shared, + "contentscript.js": contentScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let win = window.open("file_sample.html"); + await extension.awaitMessage("ready"); + + const DUMMY_STR = "dummy string to copy in content script"; + await new Promise(resolve => { + SimpleTest.waitForClipboard(DUMMY_STR, () => { + extension.sendMessage(DUMMY_STR); + }, resolve, resolve); + }); + + win.close(); + + await extension.unload(); +}); + +add_task(async function test_contentscript_clipboard_paste() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "clipboardRead", + ], + content_scripts: [{ + matches: ["http://mochi.test/*/file_sample.html"], + js: ["shared.js", "content_script.js"], + }], + }, + files: { + "shared.js": shared, + "content_script.js": () => { + browser.test.sendMessage("paste", doPaste()); + }, + }, + }); + + const STRANGE = "A Strange Thing"; + SpecialPowers.clipboardCopyString(STRANGE); + + await extension.startup(); + const win = window.open("file_sample.html"); + + const paste = await extension.awaitMessage("paste"); + is(paste, STRANGE, "the correct string was pasted"); + + win.close(); + await extension.unload(); +}); + +add_task(async function test_background_clipboard_paste() { + function background() { + browser.test.sendMessage("paste", doPaste()); + } + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["clipboardRead"], + }, + background: [shared, background], + }); + + const STRANGE = "Stranger Things"; + SpecialPowers.clipboardCopyString(STRANGE); + + await extension.startup(); + + const paste = await extension.awaitMessage("paste"); + is(paste, STRANGE, "the correct string was pasted"); + + await extension.unload(); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_clipboard_image.html b/toolkit/components/extensions/test/mochitest/test_ext_clipboard_image.html new file mode 100644 index 0000000000..b5d5f6764a --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_clipboard_image.html @@ -0,0 +1,262 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Clipboard permissions tests</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> + +<script> +"use strict"; +/** + * This cannot be a xpcshell test, because: + * - On Android, copyString of nsIClipboardHelper segfaults because + * widget/android/nsClipboard.cpp calls java::Clipboard::SetText, which is + * unavailable in xpcshell. + * - On Windows, the clipboard is unavailable to xpcshell. + */ + +function resetClipboard() { + SpecialPowers.clipboardCopyString( + "This is the default value of the clipboard in the test."); +} + +async function checkClipboardHasTestImage(imageType) { + async function backgroundScript(imageType) { + async function verifyImage(img) { + // Checks whether the image is a 1x1 red image. + browser.test.assertEq(1, img.naturalWidth, "image width should match"); + browser.test.assertEq(1, img.naturalHeight, "image height should match"); + + let canvas = document.createElement("canvas"); + canvas.width = 1; + canvas.height = 1; + let ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); // Draw without scaling. + let [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data; + let expectedColor; + if (imageType === "png") { + expectedColor = [255, 0, 0]; + } else if (imageType === "jpeg") { + expectedColor = [254, 0, 0]; + } + let {os} = await browser.runtime.getPlatformInfo(); + if (os === "mac") { + // Due to https://bugzil.la/1396587, the pasted image differs from the + // original/expected image. + // Once that bug is fixed, this whole macOS-only branch can be removed. + if (imageType === "png") { + expectedColor = [255, 38, 0]; + } else if (imageType === "jpeg") { + expectedColor = [255, 38, 0]; + } + } + browser.test.assertEq(expectedColor[0], r, "pixel should be red"); + browser.test.assertEq(expectedColor[1], g, "pixel should not contain green"); + browser.test.assertEq(expectedColor[2], b, "pixel should not contain blue"); + browser.test.assertEq(255, a, "pixel should be opaque"); + } + + let editable = document.body; + editable.contentEditable = true; + let file; + await new Promise(resolve => { + document.addEventListener("paste", function(event) { + browser.test.assertEq(1, event.clipboardData.types.length, "expected one type"); + browser.test.assertEq("Files", event.clipboardData.types[0], "expected type"); + browser.test.assertEq(1, event.clipboardData.files.length, "expected one file"); + + // After returning from the paste event, event.clipboardData is cleaned, so we + // have to store the file in a separate variable. + file = event.clipboardData.files[0]; + resolve(); + }, {once: true}); + + document.execCommand("paste"); // requires clipboardWrite permission. + }); + + // When image data is copied, its first frame is decoded and exported to the + // clipboard. The pasted result is always an unanimated PNG file, regardless + // of the input. + browser.test.assertEq("image/png", file.type, "expected file.type"); + + // event.files[0] should be an accurate representation of the input image. + { + let img = new Image(); + await new Promise((resolve, reject) => { + img.onload = resolve; + img.onerror = () => reject(new Error(`Failed to load image ${img.src} of size ${file.size}`)); + img.src = URL.createObjectURL(file); + }); + + await verifyImage(img); + } + + // This confirms that an image was put on the clipboard. + // In contrast, when document.execCommand('copy') + clipboardData.setData + // is used, then the 'paste' event will also have the image data (as tested + // above), but the contentEditable area will be empty. + { + let imgs = editable.querySelectorAll("img"); + browser.test.assertEq(1, imgs.length, "should have pasted one image"); + await verifyImage(imgs[0]); + } + browser.test.sendMessage("tested image on clipboard"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})("${imageType}");`, + manifest: { + permissions: ["clipboardRead"], + }, + }); + await extension.startup(); + await extension.awaitMessage("tested image on clipboard"); + await extension.unload(); +} + +add_task(async function test_without_clipboard_permission() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.test.assertEq(undefined, browser.clipboard, + "clipboard API requires the clipboardWrite permission."); + browser.test.notifyPass(); + }, + manifest: { + permissions: ["clipboardRead"], + }, + }); + await extension.startup(); + await extension.awaitFinish(); + await extension.unload(); +}); + +add_task(async function test_copy_png() { + if (AppConstants.platform === "android") { + return; // Android does not support images on the clipboard. + } + let extension = ExtensionTestUtils.loadExtension({ + async background() { + // A 1x1 red PNG image. + let b64data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAABlBMVEX/AAD///9BHTQRAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAACklEQVQImWNgAAAAAgAB9HFkpgAAAABJRU5ErkJggg=="; + let imageData = Uint8Array.from(atob(b64data), c => c.charCodeAt(0)).buffer; + await browser.clipboard.setImageData(imageData, "png"); + browser.test.sendMessage("Called setImageData with PNG"); + }, + manifest: { + permissions: ["clipboardWrite"], + }, + }); + + resetClipboard(); + + await extension.startup(); + await extension.awaitMessage("Called setImageData with PNG"); + await extension.unload(); + + await checkClipboardHasTestImage("png"); +}); + +add_task(async function test_copy_jpeg() { + if (AppConstants.platform === "android") { + return; // Android does not support images on the clipboard. + } + let extension = ExtensionTestUtils.loadExtension({ + async background() { + // A 1x1 red JPEG image, created using: convert xc:red red.jpg. + // JPEG is lossy, and the red pixel value is actually #FE0000 instead of + // #FF0000 (also seen using: convert red.jpg text:-). + let b64data = "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACP/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAVAQEBAAAAAAAAAAAAAAAAAAAHCf/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/ADoDFU3/2Q=="; + let imageData = Uint8Array.from(atob(b64data), c => c.charCodeAt(0)).buffer; + await browser.clipboard.setImageData(imageData, "jpeg"); + browser.test.sendMessage("Called setImageData with JPEG"); + }, + manifest: { + permissions: ["clipboardWrite"], + }, + }); + + resetClipboard(); + + await extension.startup(); + await extension.awaitMessage("Called setImageData with JPEG"); + await extension.unload(); + + await checkClipboardHasTestImage("jpeg"); +}); + +add_task(async function test_copy_invalid_image() { + if (AppConstants.platform === "android") { + // Android does not support images on the clipboard. + return; + } + let extension = ExtensionTestUtils.loadExtension({ + async background() { + // This is a PNG image. + let b64data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAABlBMVEX/AAD///9BHTQRAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAACklEQVQImWNgAAAAAgAB9HFkpgAAAABJRU5ErkJggg=="; + let pngImageData = Uint8Array.from(atob(b64data), c => c.charCodeAt(0)).buffer; + await browser.test.assertRejects( + browser.clipboard.setImageData(pngImageData, "jpeg"), + "Data is not a valid jpeg image", + "Image data that is not valid for the given type should be rejected."); + browser.test.sendMessage("finished invalid image"); + }, + manifest: { + permissions: ["clipboardWrite"], + }, + }); + + await extension.startup(); + await extension.awaitMessage("finished invalid image"); + await extension.unload(); +}); + +add_task(async function test_copy_invalid_image_type() { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + // setImageData expects "png" or "jpeg", but we pass "image/png" here. + browser.test.assertThrows( + () => { browser.clipboard.setImageData(new ArrayBuffer(0), "image/png"); }, + "Type error for parameter imageType (Invalid enumeration value \"image/png\") for clipboard.setImageData.", + "An invalid type for setImageData should be rejected."); + browser.test.sendMessage("finished invalid type"); + }, + manifest: { + permissions: ["clipboardWrite"], + }, + }); + + await extension.startup(); + await extension.awaitMessage("finished invalid type"); + await extension.unload(); +}); + +if (AppConstants.platform === "android") { + add_task(async function test_setImageData_unsupported_on_android() { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + // Android does not support images on the clipboard, + // so it should not try to decode an image but fail immediately. + await browser.test.assertRejects( + browser.clipboard.setImageData(new ArrayBuffer(0), "png"), + "Writing images to the clipboard is not supported on Android", + "Should get an error when setImageData is called on Android."); + browser.test.sendMessage("finished unsupported setImageData"); + }, + manifest: { + permissions: ["clipboardWrite"], + }, + }); + + await extension.startup(); + await extension.awaitMessage("finished unsupported setImageData"); + await extension.unload(); + }); +} + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html new file mode 100644 index 0000000000..04946ceeaf --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html @@ -0,0 +1,116 @@ +<!doctype html> +<html> +<head> + <title>Test content script match_about_blank option</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_contentscript_about_blank() { + const manifest = { + content_scripts: [ + { + match_about_blank: true, + matches: ["http://mochi.test/*/file_with_about_blank.html", "http://example.com/*"], + all_frames: true, + css: ["all.css"], + js: ["all.js"], + }, { + matches: ["http://mochi.test/*/file_with_about_blank.html"], + css: ["mochi_without.css"], + js: ["mochi_without.js"], + all_frames: true, + }, { + match_about_blank: true, + matches: ["http://mochi.test/*/file_with_about_blank.html"], + css: ["mochi_with.css"], + js: ["mochi_with.js"], + all_frames: true, + }, + ], + }; + + const files = { + "all.js": function() { + browser.runtime.sendMessage("all"); + }, + "all.css": ` + body { color: red; } + `, + "mochi_without.js": function() { + browser.runtime.sendMessage("mochi_without"); + }, + "mochi_without.css": ` + body { background: yellow; } + `, + "mochi_with.js": function() { + browser.runtime.sendMessage("mochi_with"); + }, + "mochi_with.css": ` + body { text-align: right; } + `, + }; + + function background() { + browser.runtime.onMessage.addListener((script, {url}) => { + const kind = url.startsWith("about:") ? url : "top"; + browser.test.sendMessage("script", [script, kind, url]); + browser.test.sendMessage(`${script}:${kind}`); + }); + } + + const PATH = "tests/toolkit/components/extensions/test/mochitest/file_with_about_blank.html"; + const extension = ExtensionTestUtils.loadExtension({manifest, files, background}); + await extension.startup(); + + let count = 0; + extension.onMessage("script", script => { + info(`script ran: ${script}`); + count++; + }); + + let win = window.open("http://example.com/" + PATH); + await Promise.all([ + extension.awaitMessage("all:top"), + extension.awaitMessage("all:about:blank"), + extension.awaitMessage("all:about:srcdoc"), + ]); + is(count, 3, "exactly 3 scripts ran"); + win.close(); + + win = window.open("http://mochi.test:8888/" + PATH); + await Promise.all([ + extension.awaitMessage("all:top"), + extension.awaitMessage("all:about:blank"), + extension.awaitMessage("all:about:srcdoc"), + extension.awaitMessage("mochi_without:top"), + extension.awaitMessage("mochi_with:top"), + extension.awaitMessage("mochi_with:about:blank"), + extension.awaitMessage("mochi_with:about:srcdoc"), + ]); + + let style = win.getComputedStyle(win.document.body); + is(style.color, "rgb(255, 0, 0)", "top window text color is red"); + is(style.backgroundColor, "rgb(255, 255, 0)", "top window background is yellow"); + is(style.textAlign, "right", "top window text is right-aligned"); + + let a_b = win.document.getElementById("a_b"); + style = a_b.contentWindow.getComputedStyle(a_b.contentDocument.body); + is(style.color, "rgb(255, 0, 0)", "about:blank iframe text color is red"); + is(style.backgroundColor, "rgba(0, 0, 0, 0)", "about:blank iframe background is transparent"); + is(style.textAlign, "right", "about:blank text is right-aligned"); + + is(count, 10, "exactly 7 more scripts ran"); + win.close(); + + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html new file mode 100644 index 0000000000..306f093fe1 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html @@ -0,0 +1,371 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +// Create a test extension with the provided function as the background +// script. The background script will have a few helpful functions +// available. +/* global awaitLoad, gatherFrameSources */ +function makeExtension(background) { + // Wait for a webNavigation.onCompleted event where the details for the + // loaded page match the attributes of `filter`. + function awaitLoad(filter) { + return new Promise(resolve => { + const listener = details => { + if (Object.keys(filter).every(key => details[key] === filter[key])) { + browser.webNavigation.onCompleted.removeListener(listener); + resolve(); + } + }; + browser.webNavigation.onCompleted.addListener(listener); + }); + } + + // Return a string with a (sorted) list of the source of all frames + // in the given tab into which this extension can inject scripts + // (ie all frames for which it has the activeTab permission). + // Source is the hostname for frames in http sources, or the full + // location href in other documents (eg about: pages) + async function gatherFrameSources(tabid) { + let result = await browser.tabs.executeScript(tabid, { + allFrames: true, + matchAboutBlank: true, + code: "window.location.hostname || window.location.href;", + }); + return String(result.sort()); + } + + return ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["activeTab", "webNavigation"], + }, + background: `${awaitLoad}\n${gatherFrameSources}\n${ExtensionTestCommon.serializeScript(background)}`, + }); +} + +// Test that executeScript() fails without the activeTab permission +// (or any specific origin permissions). +add_task(async function test_no_activeTab() { + let extension = makeExtension(async function background() { + const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html"; + + let [tab] = await Promise.all([ + browser.tabs.create({url: URL}), + awaitLoad({frameId: 0}), + ]); + + try { + await gatherFrameSources(tab.id); + browser.test.fail("executeScript() should fail without activeTab permission"); + } catch (err) { + browser.test.assertTrue(/^Missing host permission/.test(err.message), + "executeScript() without activeTab permission failed"); + } + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("no-active-tab"); + }); + + await extension.startup(); + await extension.awaitFinish("no-active-tab"); + await extension.unload(); +}); + +// Test that dynamically created iframes do not get the activeTab permission +add_task(async function test_dynamic_frames() { + let extension = makeExtension(async function background() { + const BASE_HOST = "www.example.com"; + + let [tab] = await Promise.all([ + browser.tabs.create({url: `http://${BASE_HOST}/`}), + awaitLoad({frameId: 0}), + ]); + + function inject() { + let nframes = 4; + function frameLoaded() { + nframes--; + if (nframes == 0) { + browser.runtime.sendMessage("frames-loaded"); + } + } + + let frame = document.createElement("iframe"); + frame.addEventListener("load", frameLoaded, {once: true}); + document.body.appendChild(frame); + + let div = document.createElement("div"); + div.innerHTML = "<iframe src='http://test1.example.com/'></iframe>"; + let framelist = div.getElementsByTagName("iframe"); + browser.test.assertEq(1, framelist.length, "Found 1 frame inside div"); + framelist[0].addEventListener("load", frameLoaded, {once: true}); + document.body.appendChild(div); + + let div2 = document.createElement("div"); + div2.innerHTML = "<iframe srcdoc=\"<iframe src='http://test2.example.com/'></iframe>\"></iframe>"; + framelist = div2.getElementsByTagName("iframe"); + browser.test.assertEq(1, framelist.length, "Found 1 frame inside div"); + framelist[0].addEventListener("load", frameLoaded, {once: true}); + document.body.appendChild(div2); + + const URL = "http://www.example.com/tests/toolkit/components/extensions/test/mochitest/file_contentscript_iframe.html"; + + let xhr = new XMLHttpRequest(); + xhr.open("GET", URL); + xhr.responseType = "document"; + xhr.overrideMimeType("text/html"); + + xhr.addEventListener("load", () => { + if (xhr.readyState != 4) { + return; + } + if (xhr.status != 200) { + browser.runtime.sendMessage("error"); + } + + let frame = xhr.response.getElementById("frame"); + browser.test.assertTrue(frame, "Found frame in response document"); + frame.addEventListener("load", frameLoaded, {once: true}); + document.body.appendChild(frame); + }, {once: true}); + xhr.addEventListener("error", () => { + browser.runtime.sendMessage("error"); + }, {once: true}); + xhr.send(); + } + + browser.test.onMessage.addListener(async () => { + let loadedPromise = new Promise((resolve, reject) => { + let listener = msg => { + let unlisten = () => browser.runtime.onMessage.removeListener(listener); + if (msg == "frames-loaded") { + unlisten(); + resolve(); + } else if (msg == "error") { + unlisten(); + reject(); + } + }; + browser.runtime.onMessage.addListener(listener); + }); + + await browser.tabs.executeScript(tab.id, { + code: `(${inject})();`, + }); + + await loadedPromise; + + let result = await gatherFrameSources(tab.id); + browser.test.assertEq(String([BASE_HOST]), result, + "Script is not injected into dynamically created frames"); + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("dynamic-frames"); + }); + + browser.test.sendMessage("ready", tab.id); + }); + + await extension.startup(); + + let tabId = await extension.awaitMessage("ready"); + extension.grantActiveTab(tabId); + + extension.sendMessage("go"); + await extension.awaitFinish("dynamic-frames"); + + await extension.unload(); +}); + +// Test that an iframe created from an <iframe srcdoc> gets the +// activeTab permission. +add_task(async function test_srcdoc() { + let extension = makeExtension(async function background() { + const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab2.html"; + const OUTER_SOURCE = "about:srcdoc"; + const PAGE_SOURCE = "mochi.test"; + const FRAME_SOURCE = "test1.example.com"; + + let [tab] = await Promise.all([ + browser.tabs.create({url: URL}), + awaitLoad({frameId: 0}), + ]); + + browser.test.onMessage.addListener(async msg => { + if (msg == "go") { + let result = await gatherFrameSources(tab.id); + browser.test.assertEq(String([OUTER_SOURCE, PAGE_SOURCE, FRAME_SOURCE]), + result, + "Script is injected into frame created from <iframe srcdoc>"); + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("srcdoc"); + } + }); + + browser.test.sendMessage("ready", tab.id); + }); + + await extension.startup(); + + let tabId = await extension.awaitMessage("ready"); + extension.grantActiveTab(tabId); + + extension.sendMessage("go"); + await extension.awaitFinish("srcdoc"); + + await extension.unload(); +}); + +// Test that navigating frames by setting the src attribute from the +// parent page revokes the activeTab permission. +add_task(async function test_navigate_by_src() { + let extension = makeExtension(async function background() { + const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html"; + const PAGE_SOURCE = "mochi.test"; + const EMPTY_SOURCE = "about:blank"; + const FRAME_SOURCE = "test1.example.com"; + + let [tab] = await Promise.all([ + browser.tabs.create({url: URL}), + awaitLoad({frameId: 0}), + ]); + + browser.test.onMessage.addListener(async msg => { + if (msg == "go") { + let result = await gatherFrameSources(tab.id); + browser.test.assertEq(String([EMPTY_SOURCE, PAGE_SOURCE, FRAME_SOURCE]), + result, + "In original page, script is injected into base page and original frames"); + + let loadedPromise = awaitLoad({tabId: tab.id}); + await browser.tabs.executeScript(tab.id, { + code: "document.getElementById('emptyframe').src = 'http://test2.example.com/';", + }); + await loadedPromise; + + result = await gatherFrameSources(tab.id); + browser.test.assertEq(String([PAGE_SOURCE, FRAME_SOURCE]), result, + "Script is not injected into initially empty frame after navigation"); + + loadedPromise = awaitLoad({tabId: tab.id}); + await browser.tabs.executeScript(tab.id, { + code: "document.getElementById('regularframe').src = 'http://test2.example.com/';", + }); + await loadedPromise; + + result = await gatherFrameSources(tab.id); + browser.test.assertEq(String([PAGE_SOURCE]), result, + "Script is not injected into regular frame after navigation"); + + await browser.tabs.remove(tab.id); + browser.test.notifyPass("test-scripts"); + } + }); + + browser.test.sendMessage("ready", tab.id); + }); + + await extension.startup(); + + let tabId = await extension.awaitMessage("ready"); + extension.grantActiveTab(tabId); + + extension.sendMessage("go"); + await extension.awaitFinish("test-scripts"); + + await extension.unload(); +}); + +// Test that navigating frames by setting window.location from inside the +// frame revokes the activeTab permission. +add_task(async function test_navigate_by_window_location() { + let extension = makeExtension(async function background() { + const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html"; + const PAGE_SOURCE = "mochi.test"; + const EMPTY_SOURCE = "about:blank"; + const FRAME_SOURCE = "test1.example.com"; + + let [tab] = await Promise.all([ + browser.tabs.create({url: URL}), + awaitLoad({frameId: 0}), + ]); + + browser.test.onMessage.addListener(async msg => { + if (msg == "go") { + let result = await gatherFrameSources(tab.id); + browser.test.assertEq(String([EMPTY_SOURCE, PAGE_SOURCE, FRAME_SOURCE]), + result, + "Script initially injected into all frames"); + + let nframes = 0; + let frames = await browser.webNavigation.getAllFrames({tabId: tab.id}); + for (let frame of frames) { + if (frame.parentFrameId == -1) { + continue; + } + + let loadPromise = awaitLoad({ + tabId: tab.id, + frameId: frame.frameId, + }); + + await browser.tabs.executeScript(tab.id, { + frameId: frame.frameId, + matchAboutBlank: true, + code: "window.location.href = 'https://test2.example.com/';", + }); + await loadPromise; + + try { + result = await browser.tabs.executeScript(tab.id, { + frameId: frame.frameId, + matchAboutBlank: true, + code: "window.location.hostname;", + }); + + browser.test.fail("executeScript should have failed on navigated frame"); + } catch (err) { + browser.test.assertEq("Frame not found, or missing host permission", err.message); + } + + nframes++; + } + browser.test.assertEq(2, nframes, "Found 2 frames"); + + await browser.tabs.remove(tab.id); + browser.test.notifyPass("scripted-navigation"); + } + }); + + browser.test.sendMessage("ready", tab.id); + }); + + await extension.startup(); + + let tabId = await extension.awaitMessage("ready"); + extension.grantActiveTab(tabId); + + extension.sendMessage("go"); + await extension.awaitFinish("scripted-navigation"); + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html new file mode 100644 index 0000000000..e8bb638d95 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script caching</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +// This file defines content scripts. +/* eslint-env mozilla/frame-script */ + +const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; + +add_task(async function test_contentscript_cache() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_start", + }], + + permissions: ["<all_urls>", "tabs"], + }, + + async background() { + // Force our extension instance to be initialized for the current content process. + await browser.tabs.insertCSS({code: ""}); + + browser.test.sendMessage("origin", location.origin); + }, + + files: { + "content_script.js": function() { + browser.test.sendMessage("content-script-loaded"); + }, + }, + }); + + await extension.startup(); + + let origin = await extension.awaitMessage("origin"); + let scriptUrl = `${origin}/content_script.js`; + + let {ExtensionManager} = SpecialPowers.Cu.import("resource://gre/modules/ExtensionChild.jsm", {}); + let ext = ExtensionManager.extensions.get(extension.id); + + ext.staticScripts.expiryTimeout = 3000; + is(ext.staticScripts.size, 0, "Should have no cached scripts"); + + let win = window.open(`${BASE}/file_sample.html`); + await extension.awaitMessage("content-script-loaded"); + + if (AppConstants.platform !== "android") { + is(ext.staticScripts.size, 1, "Should have one cached script"); + ok(ext.staticScripts.has(scriptUrl), "Script cache should contain script URL"); + } + + let chromeScript, chromeScriptDone; + let {appinfo} = SpecialPowers.Services; + if (appinfo.processType === appinfo.PROCESS_TYPE_CONTENT) { + /* globals addMessageListener, assert */ + chromeScript = SpecialPowers.loadChromeScript(() => { + addMessageListener("check-script-cache", extensionId => { + let {ExtensionManager} = ChromeUtils.import("resource://gre/modules/ExtensionChild.jsm", null); + let ext = ExtensionManager.extensions.get(extensionId); + + if (ext && ext.staticScripts) { + assert.equal(ext.staticScripts.size, 0, "Should have no cached scripts in the parent process"); + } + + sendAsyncMessage("done"); + }); + }); + chromeScript.sendAsyncMessage("check-script-cache", extension.id); + chromeScriptDone = chromeScript.promiseOneMessage("done"); + } + + SimpleTest.requestFlakyTimeout("Required to test expiry timeout"); + await new Promise(resolve => setTimeout(resolve, 3000)); + is(ext.staticScripts.size, 0, "Should have no cached scripts"); + + if (chromeScript) { + await chromeScriptDone; + chromeScript.destroy(); + } + + win.close(); + + win = window.open(`${BASE}/file_sample.html`); + await extension.awaitMessage("content-script-loaded"); + + is(ext.staticScripts.size, 1, "Should have one cached script"); + ok(ext.staticScripts.has(scriptUrl)); + + SpecialPowers.Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize"); + + is(ext.staticScripts.size, 0, "Should have no cached scripts after heap-minimize"); + + win.close(); + + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_canvas.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_canvas.html new file mode 100644 index 0000000000..c4b6b5a256 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_canvas.html @@ -0,0 +1,138 @@ +<!doctype html> +<html> +<head> + <title>Test content script access to canvas drawWindow()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script> +"use strict"; + +add_task(async function test_drawWindow() { + const permissions = [ + "<all_urls>", + ]; + + const content_scripts = [{ + matches: ["https://example.org/*"], + js: ["content_script.js"], + }]; + + const files = { + "content_script.js": () => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + try { + ctx.drawWindow(window, 0, 0, 10, 10, "red"); + const {data} = ctx.getImageData(0, 0, 10, 10); + browser.test.sendMessage("success", data.slice(0, 3).join()); + } catch (e) { + browser.test.sendMessage("error", e.message); + } + }, + }; + + const first = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + applications: { gecko: { id: "draw_window_first@tests.mozilla.org" } }, + permissions, + content_scripts + }, + files + }); + const second = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + applications: { gecko: { id: "draw_window_second@tests.mozilla.org" } }, + content_scripts + }, + files + }); + + await first.startup(); + await second.startup(); + + const win = window.open("https://example.org/tests/toolkit/components/extensions/test/mochitest/file_to_drawWindow.html"); + + const colour = await first.awaitMessage("success"); + is(colour, "255,255,153", "drawWindow() call was successful: #ff9 == rgb(255,255,153)"); + + const error = await second.awaitMessage("error"); + is(error, "ctx.drawWindow is not a function", "drawWindow() method not awailable without permission"); + + win.close(); + await first.unload(); + await second.unload(); +}); + +add_task(async function test_tainted_canvas() { + const permissions = [ + "<all_urls>", + ]; + + const content_scripts = [{ + matches: ["https://example.org/*"], + js: ["content_script.js"], + }]; + + const files = { + "content_script.js": () => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const img = new Image(); + + img.onload = function() { + ctx.drawImage(img, 0, 0); + try { + const png = canvas.toDataURL(); + const {data} = ctx.getImageData(0, 0, 10, 10); + browser.test.sendMessage("success", {png, colour: data.slice(0, 4).join()}); + } catch (e) { + browser.test.log(`Exception: ${e.message}`); + browser.test.sendMessage("error", e.message); + } + }; + + // Cross-origin image from example.com. + img.src = "https://example.com/tests/toolkit/components/extensions/test/mochitest/file_image_good.png"; + }, + }; + + const first = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + applications: { gecko: { id: "draw_window_first@tests.mozilla.org" } }, + permissions, + content_scripts + }, + files + }); + const second = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + applications: { gecko: { id: "draw_window_second@tests.mozilla.org" } }, + content_scripts + }, + files + }); + + await first.startup(); + await second.startup(); + + const win = window.open("https://example.org/tests/toolkit/components/extensions/test/mochitest/file_to_drawWindow.html"); + + const {png, colour} = await first.awaitMessage("success"); + ok(png.startsWith("data:image/png;base64,"), "toDataURL() call was successful."); + is(colour, "0,0,0,0", "getImageData() returned the correct colour (transparent)."); + + const error = await second.awaitMessage("error"); + is(error, "The operation is insecure.", "toDataURL() throws without permission."); + + win.close(); + await first.unload(); + await second.unload(); +}); + +</script> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html new file mode 100644 index 0000000000..f2a2de0e05 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Sandbox metadata on WebExtensions ContentScripts</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_contentscript_devtools_sandbox_metadata() { + function contentScript() { + browser.runtime.sendMessage("contentScript.executed"); + } + + function background() { + browser.runtime.onMessage.addListener((msg) => { + if (msg == "contentScript.executed") { + browser.test.notifyPass("contentScript.executed"); + } + }); + } + + let extensionData = { + manifest: { + content_scripts: [ + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_idle", + }, + ], + }, + + background, + files: { + "content_script.js": contentScript, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + + let win = window.open("file_sample.html"); + + let innerWindowID = SpecialPowers.wrap(win).windowGlobalChild.innerWindowId; + + await extension.awaitFinish("contentScript.executed"); + + const {ExtensionContent} = SpecialPowers.Cu.import( + "resource://gre/modules/ExtensionContent.jsm", {} + ); + + let res = ExtensionContent.getContentScriptGlobals(win); + is(res.length, 1, "Got the expected array of globals"); + let metadata = SpecialPowers.Cu.getSandboxMetadata(res[0]) || {}; + + is(metadata.addonId, extension.id, "Got the expected addonId"); + is(metadata["inner-window-id"], innerWindowID, "Got the expected inner-window-id"); + + await extension.unload(); + info("extension unloaded"); + + res = ExtensionContent.getContentScriptGlobals(win); + is(res.length, 0, "No content scripts globals found once the extension is unloaded"); + + win.close(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_fission_frame.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_fission_frame.html new file mode 100644 index 0000000000..702456a798 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_fission_frame.html @@ -0,0 +1,100 @@ +<!doctype html> +<head> + <title>Test content script in cross-origin frame</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<script> +"use strict"; + +add_task(async function test_content_script_cross_origin_frame() { + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [{ + matches: ["http://example.net/*/file_sample.html"], + all_frames: true, + js: ["cs.js"], + }], + permissions: ["http://example.net/"], + }, + + background() { + browser.runtime.onConnect.addListener(port => { + port.onMessage.addListener(async num => { + let { tab, url, frameId } = port.sender; + + browser.test.assertTrue(frameId > 0, "sender frameId is ok"); + browser.test.assertTrue(url.endsWith("file_sample.html"), "url is ok"); + + let shared = await browser.tabs.executeScript(tab.id, { + allFrames: true, + code: `window.sharedVal`, + }); + browser.test.assertEq(shared[0], 357, "CS runs in a shared Sandbox"); + + let code = "does.not.exist"; + await browser.test.assertRejects( + browser.tabs.executeScript(tab.id, { allFrames: true, code }), + /does is not defined/, + "Got the expected rejection from tabs.executeScript" + ); + + code = "() => {}"; + await browser.test.assertRejects( + browser.tabs.executeScript(tab.id, { allFrames: true, code }), + /Script .* result is non-structured-clonable data/, + "Got the expected rejection from tabs.executeScript" + ); + + let result = await browser.tabs.sendMessage(tab.id, num); + port.postMessage(result); + port.disconnect(); + }); + }); + }, + + files: { + "cs.js"() { + let text = document.body.innerText; + browser.test.assertEq(text, "Sample text", "CS can access page DOM"); + + let manifest = browser.runtime.getManifest(); + browser.test.assertEq(manifest.version, "1.0"); + browser.test.assertEq(manifest.name, "Generated extension"); + + browser.runtime.onMessage.addListener(async num => { + browser.test.log("content script received tabs.sendMessage"); + return num * 3; + }) + + let response; + window.sharedVal = 357; + + let port = browser.runtime.connect(); + port.onMessage.addListener(num => { + response = num; + }); + port.onDisconnect.addListener(() => { + browser.test.assertEq(response, 21, "Got correct response"); + browser.test.notifyPass(); + }); + port.postMessage(7); + }, + }, + }); + + await extension.startup(); + + let base = "http://example.org/tests/toolkit/components/extensions/test"; + let win = window.open(`${base}/mochitest/file_with_xorigin_frame.html`); + + await extension.awaitFinish(); + win.close(); + + await extension.unload(); +}); + +</script> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html new file mode 100644 index 0000000000..63dd23b151 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html @@ -0,0 +1,105 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script private browsing ID</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ChromeTask.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +async function test_contentscript_incognito() { + await SpecialPowers.pushPrefEnv({set: [ + ["extensions.allowPrivateBrowsingByDefault", false], + ]}); + + let extension = ExtensionTestUtils.loadExtension({ + incognitoOverride: "spanning", + manifest: { + content_scripts: [ + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + }, + ], + }, + + background() { + let windowId; + + browser.test.onMessage.addListener(([msg, url]) => { + if (msg === "open-window") { + browser.windows.create({url, incognito: true}).then(window => { + windowId = window.id; + }); + } else if (msg === "close-window") { + browser.windows.remove(windowId).then(() => { + browser.test.sendMessage("done"); + }); + } + }); + }, + + files: { + "content_script.js": async () => { + const COOKIE = "foo=florgheralzps"; + document.cookie = COOKIE; + + let url = new URL("return_headers.sjs", location.href); + + let responses = [ + new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.open("GET", url); + xhr.onload = () => resolve(JSON.parse(xhr.responseText)); + xhr.send(); + }), + + fetch(url, {credentials: "include"}).then(body => body.json()), + ]; + + try { + for (let response of await Promise.all(responses)) { + browser.test.assertEq(COOKIE, response.cookie, "Got expected cookie header"); + } + browser.test.notifyPass("cookies"); + } catch (e) { + browser.test.fail(`Error: ${e}`); + browser.test.notifyFail("cookies"); + } + }, + }, + }); + + await extension.startup(); + + extension.sendMessage(["open-window", SimpleTest.getTestFileURL("file_sample.html")]); + + await extension.awaitFinish("cookies"); + + extension.sendMessage(["close-window"]); + await extension.awaitMessage("done"); + + await extension.unload(); +} + +add_task(async function() { + await test_contentscript_incognito(); +}); + +add_task(async function() { + await SpecialPowers.pushPrefEnv({set: [ + ["network.cookie.cookieBehavior", 3], + ]}); + await test_contentscript_incognito(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html new file mode 100644 index 0000000000..e6bc48800c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_contentscript() { + function background() { + browser.test.onMessage.addListener(async url => { + let tab = await browser.tabs.create({url}); + + let executed = true; + try { + await browser.tabs.executeScript(tab.id, {code: "true;"}); + } catch (e) { + executed = false; + } + + await browser.tabs.remove([tab.id]); + browser.test.sendMessage("executed", executed); + }); + } + + let extensionData = { + useAddonManager: "permanent", + manifest: { + applications: { gecko: { id: "contentscript@tests.mozilla.org" } }, + permissions: ["<all_urls>"], + }, + background, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + extension.sendMessage("https://example.com"); + let result = await extension.awaitMessage("executed"); + is(result, true, "Content script can be run in a page without mozAddonManager"); + + await SpecialPowers.pushPrefEnv({ + set: [["extensions.webapi.testing", true]], + }); + + extension.sendMessage("https://example.com"); + result = await extension.awaitMessage("executed"); + is(result, false, "Content script cannot be run in a page with mozAddonManager"); + + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies.html new file mode 100644 index 0000000000..c9dd05a41c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies.html @@ -0,0 +1,366 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_cookies() { + await SpecialPowers.pushPrefEnv({set: [ + ["extensions.allowPrivateBrowsingByDefault", false], + ]}); + + async function background() { + function assertExpected(expected, cookie) { + for (let key of Object.keys(cookie)) { + browser.test.assertTrue(key in expected, `found property ${key}`); + browser.test.assertEq(expected[key], cookie[key], `property value for ${key} is correct`); + } + browser.test.assertEq(Object.keys(expected).length, Object.keys(cookie).length, "all expected properties found"); + } + + async function getDocumentCookie(tabId) { + let results = await browser.tabs.executeScript(tabId, { + code: "document.cookie", + }); + browser.test.assertEq(1, results.length, "executeScript returns one result"); + return results[0]; + } + + async function testIpCookie(ipAddress, setHostOnly) { + const IP_TEST_HOST = ipAddress; + const IP_TEST_URL = `http://${IP_TEST_HOST}/`; + const IP_THE_FUTURE = Date.now() + 5 * 60; + const IP_STORE_ID = "firefox-default"; + + let expectedCookie = { + name: "name1", + value: "value1", + domain: IP_TEST_HOST, + hostOnly: true, + path: "/", + secure: false, + httpOnly: false, + sameSite: "no_restriction", + session: false, + expirationDate: IP_THE_FUTURE, + storeId: IP_STORE_ID, + firstPartyDomain: "", + }; + + await browser.browsingData.removeCookies({}); + let ip_cookie = await browser.cookies.set({ + url: IP_TEST_URL, + domain: setHostOnly ? ipAddress : undefined, + name: "name1", + value: "value1", + expirationDate: IP_THE_FUTURE, + }); + assertExpected(expectedCookie, ip_cookie); + + let ip_cookies = await browser.cookies.getAll({name: "name1"}); + browser.test.assertEq(1, ip_cookies.length, "ip cookie can be added"); + assertExpected(expectedCookie, ip_cookies[0]); + + ip_cookies = await browser.cookies.getAll({domain: IP_TEST_HOST, name: "name1"}); + browser.test.assertEq(1, ip_cookies.length, "can get ip cookie by host"); + assertExpected(expectedCookie, ip_cookies[0]); + + let ip_details = await browser.cookies.remove({url: IP_TEST_URL, name: "name1"}); + assertExpected({url: IP_TEST_URL, name: "name1", storeId: IP_STORE_ID, firstPartyDomain: ""}, ip_details); + + ip_cookies = await browser.cookies.getAll({name: "name1"}); + browser.test.assertEq(0, ip_cookies.length, "ip cookie can be removed"); + } + + async function openPrivateWindowAndTab(TEST_URL) { + // Add some random suffix to make sure that we select the right tab. + const PRIVATE_TEST_URL = TEST_URL + "?random" + Math.random(); + + let tabReadyPromise = new Promise((resolve) => { + browser.webNavigation.onDOMContentLoaded.addListener(function listener({tabId}) { + browser.webNavigation.onDOMContentLoaded.removeListener(listener); + resolve(tabId); + }, { + url: [{ + urlPrefix: PRIVATE_TEST_URL, + }], + }); + }); + // This tab is opened for two purposes: + // 1. To allow tests to run content scripts in the context of a tab, + // for fetching the value of document.cookie. + // 2. TODO Bug 1309637 To work around cookies in incognito windows, + // based on the analysis in comment 8. + let {id: windowId} = await browser.windows.create({ + incognito: true, + url: PRIVATE_TEST_URL, + }); + let tabId = await tabReadyPromise; + return {windowId, tabId}; + } + + function changePort(href, port) { + let url = new URL(href); + url.port = port; + return url.href; + } + + await testIpCookie("[2a03:4000:6:310e:216:3eff:fe53:99b]", false); + await testIpCookie("[2a03:4000:6:310e:216:3eff:fe53:99b]", true); + await testIpCookie("192.168.1.1", false); + await testIpCookie("192.168.1.1", true); + + const TEST_URL = "http://example.org/"; + const TEST_SECURE_URL = "https://example.org/"; + const THE_FUTURE = Date.now() + 5 * 60; + const TEST_PATH = "set_path"; + const TEST_URL_WITH_PATH = TEST_URL + TEST_PATH; + const TEST_COOKIE_PATH = `/${TEST_PATH}`; + const STORE_ID = "firefox-default"; + const PRIVATE_STORE_ID = "firefox-private"; + + let expected = { + name: "name1", + value: "value1", + domain: "example.org", + hostOnly: true, + path: "/", + secure: false, + httpOnly: false, + sameSite: "no_restriction", + session: false, + expirationDate: THE_FUTURE, + storeId: STORE_ID, + firstPartyDomain: "", + }; + + // Remove all cookies before starting the test. + await browser.browsingData.removeCookies({}); + + let cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", expirationDate: THE_FUTURE}); + assertExpected(expected, cookie); + + cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); + assertExpected(expected, cookie); + + let cookies = await browser.cookies.getAll({name: "name1"}); + browser.test.assertEq(1, cookies.length, "one cookie found for matching name"); + assertExpected(expected, cookies[0]); + + cookies = await browser.cookies.getAll({domain: "example.org"}); + browser.test.assertEq(1, cookies.length, "one cookie found for matching domain"); + assertExpected(expected, cookies[0]); + + cookies = await browser.cookies.getAll({domain: "example.net"}); + browser.test.assertEq(0, cookies.length, "no cookies found for non-matching domain"); + + cookies = await browser.cookies.getAll({secure: false}); + browser.test.assertEq(1, cookies.length, "one non-secure cookie found"); + assertExpected(expected, cookies[0]); + + cookies = await browser.cookies.getAll({secure: true}); + browser.test.assertEq(0, cookies.length, "no secure cookies found"); + + cookies = await browser.cookies.getAll({storeId: STORE_ID}); + browser.test.assertEq(1, cookies.length, "one cookie found for valid storeId"); + assertExpected(expected, cookies[0]); + + cookies = await browser.cookies.getAll({storeId: "invalid_id"}); + browser.test.assertEq(0, cookies.length, "no cookies found for invalid storeId"); + + let details = await browser.cookies.remove({url: TEST_URL, name: "name1"}); + assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID, firstPartyDomain: ""}, details); + + cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); + browser.test.assertEq(null, cookie, "removed cookie not found"); + + // Ports in cookie URLs should be ignored. Every API call uses a different port number for better coverage. + cookie = await browser.cookies.set({url: changePort(TEST_URL, 1234), name: "name1", value: "value1", expirationDate: THE_FUTURE}); + assertExpected(expected, cookie); + + cookie = await browser.cookies.get({url: changePort(TEST_URL, 65535), name: "name1"}); + assertExpected(expected, cookie); + + cookies = await browser.cookies.getAll({url: TEST_URL}); + browser.test.assertEq(cookies.length, 1, "Found cookie using getAll without port"); + assertExpected(expected, cookies[0]); + + cookies = await browser.cookies.getAll({url: changePort(TEST_URL, 1)}); + browser.test.assertEq(cookies.length, 1, "Found cookie using getAll with port"); + assertExpected(expected, cookies[0]); + + // .remove should return the URL of the API call, so the port is included in the return value. + const TEST_URL_TO_REMOVE = changePort(TEST_URL, 1023); + details = await browser.cookies.remove({url: TEST_URL_TO_REMOVE, name: "name1"}); + assertExpected({url: TEST_URL_TO_REMOVE, name: "name1", storeId: STORE_ID, firstPartyDomain: ""}, details); + + cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); + browser.test.assertEq(null, cookie, "removed cookie not found"); + + let stores = await browser.cookies.getAllCookieStores(); + browser.test.assertEq(1, stores.length, "expected number of stores returned"); + browser.test.assertEq(STORE_ID, stores[0].id, "expected store id returned"); + browser.test.assertEq(1, stores[0].tabIds.length, "one tabId returned for store"); + browser.test.assertEq("number", typeof stores[0].tabIds[0], "tabId is a number"); + + // TODO bug 1372178: Opening private windows/tabs is not supported on Android + if (browser.windows) { + let {windowId} = await openPrivateWindowAndTab(TEST_URL); + let stores = await browser.cookies.getAllCookieStores(); + + browser.test.assertEq(2, stores.length, "expected number of stores returned"); + browser.test.assertEq(STORE_ID, stores[0].id, "expected store id returned"); + browser.test.assertEq(1, stores[0].tabIds.length, "one tab returned for store"); + browser.test.assertEq(PRIVATE_STORE_ID, stores[1].id, "expected private store id returned"); + browser.test.assertEq(1, stores[0].tabIds.length, "one tab returned for private store"); + + await browser.windows.remove(windowId); + } + + cookie = await browser.cookies.set({url: TEST_URL, name: "name2", domain: ".example.org", expirationDate: THE_FUTURE}); + browser.test.assertEq(false, cookie.hostOnly, "cookie is not a hostOnly cookie"); + + details = await browser.cookies.remove({url: TEST_URL, name: "name2"}); + assertExpected({url: TEST_URL, name: "name2", storeId: STORE_ID, firstPartyDomain: ""}, details); + + // Create a session cookie. + cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1"}); + browser.test.assertEq(true, cookie.session, "session cookie set"); + + cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); + browser.test.assertEq(true, cookie.session, "got session cookie"); + + cookies = await browser.cookies.getAll({session: true}); + browser.test.assertEq(1, cookies.length, "one session cookie found"); + browser.test.assertEq(true, cookies[0].session, "found session cookie"); + + cookies = await browser.cookies.getAll({session: false}); + browser.test.assertEq(0, cookies.length, "no non-session cookies found"); + + details = await browser.cookies.remove({url: TEST_URL, name: "name1"}); + assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID, firstPartyDomain: ""}, details); + + cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); + browser.test.assertEq(null, cookie, "removed cookie not found"); + + cookie = await browser.cookies.set({url: TEST_SECURE_URL, name: "name1", value: "value1", secure: true}); + browser.test.assertEq(true, cookie.secure, "secure cookie set"); + + cookie = await browser.cookies.get({url: TEST_SECURE_URL, name: "name1"}); + browser.test.assertEq(true, cookie.session, "got secure cookie"); + + cookies = await browser.cookies.getAll({secure: true}); + browser.test.assertEq(1, cookies.length, "one secure cookie found"); + browser.test.assertEq(true, cookies[0].secure, "found secure cookie"); + + cookies = await browser.cookies.getAll({secure: false}); + browser.test.assertEq(0, cookies.length, "no non-secure cookies found"); + + details = await browser.cookies.remove({url: TEST_SECURE_URL, name: "name1"}); + assertExpected({url: TEST_SECURE_URL, name: "name1", storeId: STORE_ID, firstPartyDomain: ""}, details); + + cookie = await browser.cookies.get({url: TEST_SECURE_URL, name: "name1"}); + browser.test.assertEq(null, cookie, "removed cookie not found"); + + cookie = await browser.cookies.set({url: TEST_URL_WITH_PATH, path: TEST_COOKIE_PATH, name: "name1", value: "value1", expirationDate: THE_FUTURE}); + browser.test.assertEq(TEST_COOKIE_PATH, cookie.path, "created cookie with path"); + + cookie = await browser.cookies.get({url: TEST_URL_WITH_PATH, name: "name1"}); + browser.test.assertEq(TEST_COOKIE_PATH, cookie.path, "got cookie with path"); + + cookies = await browser.cookies.getAll({path: TEST_COOKIE_PATH}); + browser.test.assertEq(1, cookies.length, "one cookie with path found"); + browser.test.assertEq(TEST_COOKIE_PATH, cookies[0].path, "found cookie with path"); + + cookie = await browser.cookies.get({url: TEST_URL + "invalid_path", name: "name1"}); + browser.test.assertEq(null, cookie, "get with invalid path returns null"); + + cookies = await browser.cookies.getAll({path: "/invalid_path"}); + browser.test.assertEq(0, cookies.length, "getAll with invalid path returns 0 cookies"); + + details = await browser.cookies.remove({url: TEST_URL_WITH_PATH, name: "name1"}); + assertExpected({url: TEST_URL_WITH_PATH, name: "name1", storeId: STORE_ID, firstPartyDomain: ""}, details); + + cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", httpOnly: true}); + browser.test.assertEq(true, cookie.httpOnly, "httpOnly cookie set"); + + cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", httpOnly: false}); + browser.test.assertEq(false, cookie.httpOnly, "non-httpOnly cookie set"); + + details = await browser.cookies.remove({url: TEST_URL, name: "name1"}); + assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID, firstPartyDomain: ""}, details); + + cookie = await browser.cookies.set({url: TEST_URL}); + browser.test.assertEq("", cookie.name, "default name set"); + browser.test.assertEq("", cookie.value, "default value set"); + browser.test.assertEq(true, cookie.session, "no expiry date created session cookie"); + + // TODO bug 1372178: Opening private windows/tabs is not supported on Android + if (browser.windows) { + let {tabId, windowId} = await openPrivateWindowAndTab(TEST_URL); + + browser.test.assertEq("", await getDocumentCookie(tabId), "initially no cookie"); + + let cookie = await browser.cookies.set({url: TEST_URL, name: "store", value: "private", expirationDate: THE_FUTURE, storeId: PRIVATE_STORE_ID}); + browser.test.assertEq("private", cookie.value, "set the private cookie"); + + cookie = await browser.cookies.set({url: TEST_URL, name: "store", value: "default", expirationDate: THE_FUTURE, storeId: STORE_ID}); + browser.test.assertEq("default", cookie.value, "set the default cookie"); + + cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID}); + browser.test.assertEq("private", cookie.value, "get the private cookie"); + browser.test.assertEq(PRIVATE_STORE_ID, cookie.storeId, "get the private cookie storeId"); + + cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: STORE_ID}); + browser.test.assertEq("default", cookie.value, "get the default cookie"); + browser.test.assertEq(STORE_ID, cookie.storeId, "get the default cookie storeId"); + + browser.test.assertEq("store=private", await getDocumentCookie(tabId), "private document.cookie should be set"); + + let details = await browser.cookies.remove({url: TEST_URL, name: "store", storeId: STORE_ID}); + assertExpected({url: TEST_URL, name: "store", storeId: STORE_ID, firstPartyDomain: ""}, details); + + cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: STORE_ID}); + browser.test.assertEq(null, cookie, "deleted the default cookie"); + + details = await browser.cookies.remove({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID}); + assertExpected({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID, firstPartyDomain: ""}, details); + + cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID}); + browser.test.assertEq(null, cookie, "deleted the private cookie"); + + browser.test.assertEq("", await getDocumentCookie(tabId), "private document.cookie should be removed"); + + await browser.windows.remove(windowId); + } + + browser.test.notifyPass("cookies"); + } + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + incognitoOverride: "spanning", + background, + manifest: { + applications: { gecko: { id: "cookies@tests.mozilla.org" } }, + permissions: ["cookies", "*://example.org/", "*://[2a03:4000:6:310e:216:3eff:fe53:99b]/", "*://192.168.1.1/", "webNavigation", "browsingData"], + }, + }); + + await extension.startup(); + await extension.awaitFinish("cookies"); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html new file mode 100644 index 0000000000..db12a97854 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function setup() { + // make sure userContext is enabled. + await SpecialPowers.pushPrefEnv({"set": [ + ["privacy.userContext.enabled", true], + ]}); +}); + +add_task(async function test_cookie_containers() { + async function background() { + // Sometimes there is a cookie without name/value when running tests. + let cookiesAtStart = await browser.cookies.getAll({storeId: "firefox-default"}); + + function assertExpected(expected, cookie) { + for (let key of Object.keys(cookie)) { + browser.test.assertTrue(key in expected, `found property ${key}`); + browser.test.assertEq(expected[key], cookie[key], `property value for ${key} is correct`); + } + browser.test.assertEq(Object.keys(expected).length, Object.keys(cookie).length, "all expected properties found"); + } + + const TEST_URL = "http://example.org/"; + const THE_FUTURE = Date.now() + 5 * 60; + + let expected = { + name: "name1", + value: "value1", + domain: "example.org", + hostOnly: true, + path: "/", + secure: false, + httpOnly: false, + sameSite: "no_restriction", + session: false, + expirationDate: THE_FUTURE, + storeId: "firefox-container-1", + firstPartyDomain: "", + }; + + let cookie = await browser.cookies.set({ + url: TEST_URL, name: "name1", value: "value1", + expirationDate: THE_FUTURE, storeId: "firefox-container-1", + }); + browser.test.assertEq("firefox-container-1", cookie.storeId, "the cookie has the correct storeId"); + + cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); + browser.test.assertEq(null, cookie, "get() without storeId returns null"); + + cookie = await browser.cookies.get({url: TEST_URL, name: "name1", storeId: "firefox-container-1"}); + assertExpected(expected, cookie); + + let cookies = await browser.cookies.getAll({storeId: "firefox-default"}); + browser.test.assertEq(0, cookiesAtStart.length - cookies.length, "getAll() with default storeId hasn't added cookies"); + + cookies = await browser.cookies.getAll({storeId: "firefox-container-1"}); + browser.test.assertEq(1, cookies.length, "one cookie found for matching domain"); + assertExpected(expected, cookies[0]); + + let details = await browser.cookies.remove({url: TEST_URL, name: "name1", storeId: "firefox-container-1"}); + assertExpected({url: TEST_URL, name: "name1", storeId: "firefox-container-1", firstPartyDomain: ""}, details); + + cookie = await browser.cookies.get({url: TEST_URL, name: "name1", storeId: "firefox-container-1"}); + browser.test.assertEq(null, cookie, "removed cookie not found"); + + browser.test.notifyPass("cookies"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["cookies", "*://example.org/"], + }, + }); + + await extension.startup(); + await extension.awaitFinish("cookies"); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html new file mode 100644 index 0000000000..fa118f5271 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension cookies test</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_cookies_expiry() { + function background() { + let expectedEvents = []; + + browser.cookies.onChanged.addListener(event => { + expectedEvents.push(`${event.removed}:${event.cause}`); + if (expectedEvents.length === 1) { + browser.test.assertEq("true:expired", expectedEvents[0], "expired cookie removed"); + browser.test.assertEq("first", event.cookie.name, "expired cookie has the expected name"); + browser.test.assertEq("one", event.cookie.value, "expired cookie has the expected value"); + } else { + browser.test.assertEq("false:explicit", expectedEvents[1], "new cookie added"); + browser.test.assertEq("first", event.cookie.name, "new cookie has the expected name"); + browser.test.assertEq("one-again", event.cookie.value, "new cookie has the expected value"); + browser.test.notifyPass("cookie-expiry"); + } + }); + + setTimeout(() => { + browser.test.sendMessage("change-cookies"); + }, 1000); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["http://example.com/", "cookies"], + }, + background, + }); + + let chromeScript = loadChromeScript(() => { + const {sendAsyncMessage} = this; + Services.cookies.add(".example.com", "/", "first", "one", false, false, false, Date.now() / 1000 + 1, {}, Ci.nsICookie.SAMESITE_NONE, Ci.nsICookie.SCHEME_HTTP); + sendAsyncMessage("done"); + }); + await chromeScript.promiseOneMessage("done"); + chromeScript.destroy(); + + await extension.startup(); + await extension.awaitMessage("change-cookies"); + + chromeScript = loadChromeScript(() => { + const {sendAsyncMessage} = this; + Services.cookies.add(".example.com", "/", "first", "one-again", false, false, false, Date.now() / 1000 + 10, {}, Ci.nsICookie.SAMESITE_NONE, Ci.nsICookie.SCHEME_HTTP); + sendAsyncMessage("done"); + }); + await chromeScript.promiseOneMessage("done"); + chromeScript.destroy(); + + await extension.awaitFinish("cookie-expiry"); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_first_party.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_first_party.html new file mode 100644 index 0000000000..7e33f4731d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_first_party.html @@ -0,0 +1,316 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> +<script src="head.js"></script> +<script> +"use strict"; + +async function background() { + const url = "http://ext-cookie-first-party.mochi.test/"; + const firstPartyDomain = "ext-cookie-first-party.mochi.test"; + // A first party domain with invalid characters for the file system, which just happens to be a IPv6 address. + const firstPartyDomainInvalidChars = "[2606:4700:4700::1111]"; + const expectedError = "First-Party Isolation is enabled, but the required 'firstPartyDomain' attribute was not set."; + + const assertExpectedCookies = (expected, cookies, message) => { + let matches = (cookie, expected) => { + if (!cookie || !expected) { + return cookie === expected; // true if both are null. + } + for (let key of Object.keys(expected)) { + if (cookie[key] !== expected[key]) { + return false; + } + } + return true; + }; + browser.test.assertEq(expected.length, cookies.length, `Got expected number of cookies - ${message}`); + if (cookies.length !== expected.length) { + return; + } + for (let expect of expected) { + let foundCookies = cookies.filter(cookie => matches(cookie, expect)); + browser.test.assertEq(1, foundCookies.length, + `Expected cookie ${JSON.stringify(expect)} found - ${message}`); + } + }; + + // Test when FPI is disabled. + const test_fpi_disabled = async () => { + let cookie, cookies; + + // set + cookie = await browser.cookies.set({url, name: "foo1", value: "bar1"}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], [cookie], "set: FPI off, w/ empty firstPartyDomain, non-FP cookie"); + cookie = await browser.cookies.set({url, name: "foo2", value: "bar2", firstPartyDomain}); + assertExpectedCookies([ + {name: "foo2", value: "bar2", firstPartyDomain}, + ], [cookie], "set: FPI off, w/ firstPartyDomain, FP cookie"); + + // get + // When FPI is disabled, missing key/null/undefined is equivalent to "". + cookie = await browser.cookies.get({url, name: "foo1"}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], [cookie], "get: FPI off, w/o firstPartyDomain, non-FP cookie"); + cookie = await browser.cookies.get({url, name: "foo1", firstPartyDomain: ""}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], [cookie], "get: FPI off, w/ empty firstPartyDomain, non-FP cookie"); + cookie = await browser.cookies.get({url, name: "foo1", firstPartyDomain: null}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], [cookie], "get: FPI off, w/ null firstPartyDomain, non-FP cookie"); + cookie = await browser.cookies.get({url, name: "foo1", firstPartyDomain: undefined}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], [cookie], "get: FPI off, w/ undefined firstPartyDomain, non-FP cookie"); + + cookie = await browser.cookies.get({url, name: "foo2", firstPartyDomain}); + assertExpectedCookies([ + {name: "foo2", value: "bar2", firstPartyDomain}, + ], [cookie], "get: FPI off, w/ firstPartyDomain, FP cookie"); + // There is no match for non-FP cookies with name "foo2". + cookie = await browser.cookies.get({url, name: "foo2"}); + assertExpectedCookies([null], [cookie], "get: FPI off, w/o firstPartyDomain, no cookie"); + cookie = await browser.cookies.get({url, name: "foo2", firstPartyDomain: ""}); + assertExpectedCookies([null], [cookie], "get: FPI off, w/ empty firstPartyDomain, no cookie"); + cookie = await browser.cookies.get({url, name: "foo2", firstPartyDomain: null}); + assertExpectedCookies([null], [cookie], "get: FPI off, w/ null firstPartyDomain, no cookie"); + cookie = await browser.cookies.get({url, name: "foo2", firstPartyDomain: undefined}); + assertExpectedCookies([null], [cookie], "get: FPI off, w/ undefined firstPartyDomain, no cookie"); + + // getAll + for (let extra of [{}, {url}, {domain: firstPartyDomain}]) { + const prefix = `getAll(${JSON.stringify(extra)})`; + cookies = await browser.cookies.getAll({...extra}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], cookies, `${prefix}: FPI off, w/o firstPartyDomain, non-FP cookies`); + cookies = await browser.cookies.getAll({...extra, firstPartyDomain: ""}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], cookies, `${prefix}: FPI off, w/ empty firstPartyDomain, non-FP cookies`); + cookies = await browser.cookies.getAll({...extra, firstPartyDomain: null}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + {name: "foo2", value: "bar2", firstPartyDomain}, + ], cookies, `${prefix}: FPI off, w/ null firstPartyDomain, all cookies`); + cookies = await browser.cookies.getAll({...extra, firstPartyDomain: undefined}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + {name: "foo2", value: "bar2", firstPartyDomain}, + ], cookies, `${prefix}: FPI off, w/ undefined firstPartyDomain, all cookies`); + cookies = await browser.cookies.getAll({...extra, firstPartyDomain}); + assertExpectedCookies([ + {name: "foo2", value: "bar2", firstPartyDomain}, + ], cookies, `${prefix}: FPI off, w/ firstPartyDomain, FP cookies`); + } + + // remove + cookie = await browser.cookies.remove({url, name: "foo1"}); + assertExpectedCookies([ + {url, name: "foo1", firstPartyDomain: ""}, + ], [cookie], "remove: FPI off, w/ empty firstPartyDomain, non-FP cookie"); + cookie = await browser.cookies.remove({url, name: "foo2", firstPartyDomain}); + assertExpectedCookies([ + {url, name: "foo2", firstPartyDomain}, + ], [cookie], "remove: FPI off, w/ firstPartyDomain, FP cookie"); + + // Test if FP cookies set when FPI off can be accessed when FPI on. + await browser.cookies.set({url, name: "foo1", value: "bar1"}); + await browser.cookies.set({url, name: "foo2", value: "bar2", firstPartyDomain}); + + browser.test.sendMessage("test_fpi_disabled"); + }; + + // Test when FPI is enabled. + const test_fpi_enabled = async () => { + let cookie, cookies; + + // set + await browser.test.assertRejects( + browser.cookies.set({url, name: "foo3", value: "bar3"}), + expectedError, + "set: FPI on, w/o firstPartyDomain, rejection"); + cookie = await browser.cookies.set({url, name: "foo4", value: "bar4", firstPartyDomain}); + assertExpectedCookies([ + {name: "foo4", value: "bar4", firstPartyDomain}, + ], [cookie], "set: FPI on, w/ firstPartyDomain, FP cookie"); + + // get + await browser.test.assertRejects( + browser.cookies.get({url, name: "foo3"}), + expectedError, + "get: FPI on, w/o firstPartyDomain, rejection"); + await browser.test.assertRejects( + browser.cookies.get({url, name: "foo3", firstPartyDomain: null}), + expectedError, + "get: FPI on, w/ null firstPartyDomain, rejection"); + await browser.test.assertRejects( + browser.cookies.get({url, name: "foo3", firstPartyDomain: undefined}), + expectedError, + "get: FPI on, w/ undefined firstPartyDomain, rejection"); + cookie = await browser.cookies.get({url, name: "foo1", firstPartyDomain: ""}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], [cookie], "get: FPI on, w/ empty firstPartyDomain, non-FP cookie"); + cookie = await browser.cookies.get({url, name: "foo4", firstPartyDomain}); + assertExpectedCookies([ + {name: "foo4", value: "bar4", firstPartyDomain}, + ], [cookie], "get: FPI on, w/ firstPartyDomain, FP cookie"); + cookie = await browser.cookies.get({url, name: "foo2", firstPartyDomain}); + assertExpectedCookies([ + {name: "foo2", value: "bar2", firstPartyDomain}, + ], [cookie], "get: FPI on, w/ firstPartyDomain, FP cookie (set when FPI off)"); + + // getAll + for (let extra of [{}, {url}, {domain: firstPartyDomain}]) { + const prefix = `getAll(${JSON.stringify(extra)})`; + await browser.test.assertRejects( + browser.cookies.getAll({...extra}), + expectedError, + `${prefix}: FPI on, w/o firstPartyDomain, rejection`); + cookies = await browser.cookies.getAll({...extra, firstPartyDomain: ""}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + ], cookies, `${prefix}: FPI on, w/ empty firstPartyDomain, non-FP cookies`); + cookies = await browser.cookies.getAll({...extra, firstPartyDomain: null}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + {name: "foo2", value: "bar2", firstPartyDomain}, + {name: "foo4", value: "bar4", firstPartyDomain}, + ], cookies, `${prefix}: FPI on, w/ null firstPartyDomain, all cookies`); + cookies = await browser.cookies.getAll({...extra, firstPartyDomain: undefined}); + assertExpectedCookies([ + {name: "foo1", value: "bar1", firstPartyDomain: ""}, + {name: "foo2", value: "bar2", firstPartyDomain}, + {name: "foo4", value: "bar4", firstPartyDomain}, + ], cookies, `${prefix}: FPI on, w/ undefined firstPartyDomain, all cookies`); + cookies = await browser.cookies.getAll({...extra, firstPartyDomain}); + assertExpectedCookies([ + {name: "foo2", value: "bar2", firstPartyDomain}, + {name: "foo4", value: "bar4", firstPartyDomain}, + ], cookies, `${prefix}: FPI on, w/ firstPartyDomain, FP cookies`); + } + + // remove + await browser.test.assertRejects( + browser.cookies.remove({url, name: "foo3"}), + expectedError, + "remove: FPI on, w/o firstPartyDomain, rejection"); + cookie = await browser.cookies.remove({url, name: "foo4", firstPartyDomain}); + assertExpectedCookies([ + {url, name: "foo4", firstPartyDomain}, + ], [cookie], "remove: FPI on, w/ firstPartyDomain, FP cookie"); + cookie = await browser.cookies.remove({url, name: "foo2", firstPartyDomain}); + assertExpectedCookies([ + {url, name: "foo2", firstPartyDomain}, + ], [cookie], "remove: FPI on, w/ firstPartyDomain, FP cookie (set when FPI off)"); + + // Test if FP cookies set when FPI on can be accessed when FPI off. + await browser.cookies.set({url, name: "foo4", value: "bar4", firstPartyDomain}); + + browser.test.sendMessage("test_fpi_enabled"); + }; + + // Test FPI with a first party domain with invalid characters for + // the file system. + const test_fpi_with_invalid_characters = async () => { + let cookie; + + // Test setting a cookie with a first party domain with invalid characters + // for the file system. + cookie = await browser.cookies.set({url, name: "foo5", value: "bar5", + firstPartyDomain: firstPartyDomainInvalidChars}); + assertExpectedCookies([ + {name: "foo5", value: "bar5", firstPartyDomain: firstPartyDomainInvalidChars}, + ], [cookie], "set: FPI on, w/ firstPartyDomain with invalid characters, FP cookie"); + + // Test getting a cookie with a first party domain with invalid characters + // for the file system. + cookie = await browser.cookies.get({url, name: "foo5", + firstPartyDomain: firstPartyDomainInvalidChars}); + assertExpectedCookies([ + {name: "foo5", value: "bar5", firstPartyDomain: firstPartyDomainInvalidChars}, + ], [cookie], "get: FPI on, w/ firstPartyDomain with invalid characters, FP cookie"); + + // Test removing a cookie with a first party domain with invalid characters + // for the file system. + cookie = await browser.cookies.remove({url, name: "foo5", + firstPartyDomain: firstPartyDomainInvalidChars}); + assertExpectedCookies([ + {url, name: "foo5", firstPartyDomain: firstPartyDomainInvalidChars}, + ], [cookie], "remove: FPI on, w/ firstPartyDomain with invalid characters, FP cookie"); + + browser.test.sendMessage("test_fpi_with_invalid_characters"); + }; + + // Test when FPI is disabled again, accessing FP cookies set when FPI is enabled. + const test_fpd_cookies_on_fpi_disabled = async () => { + let cookie, cookies; + cookie = await browser.cookies.get({url, name: "foo4", firstPartyDomain}); + assertExpectedCookies([ + {name: "foo4", value: "bar4", firstPartyDomain}, + ], [cookie], "get: FPI off, w/ firstPartyDomain, FP cookie (set when FPI on)"); + cookie = await browser.cookies.remove({url, name: "foo4", firstPartyDomain}); + assertExpectedCookies([ + {url, name: "foo4", firstPartyDomain}, + ], [cookie], "remove: FPI off, w/ firstPartyDomain, FP cookie (set when FPI on)"); + + // Clean up. + await browser.cookies.remove({url, name: "foo1"}); + + cookies = await browser.cookies.getAll({firstPartyDomain: null}); + assertExpectedCookies([], cookies, "Test is finishing, all cookies removed"); + + browser.test.sendMessage("test_fpd_cookies_on_fpi_disabled"); + }; + + browser.test.onMessage.addListener((message) => { + switch (message) { + case "test_fpi_disabled": return test_fpi_disabled(); + case "test_fpi_enabled": return test_fpi_enabled(); + case "test_fpi_with_invalid_characters": return test_fpi_with_invalid_characters(); + case "test_fpd_cookies_on_fpi_disabled": return test_fpd_cookies_on_fpi_disabled(); + default: return browser.test.notifyFail("unknown-message"); + } + }); +} + +function enableFirstPartyIsolation() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.firstparty.isolate", true], + ], + }); +} + +function disableFirstPartyIsolation() { + return SpecialPowers.popPrefEnv(); +} + +add_task(async () => { + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["cookies", "*://ext-cookie-first-party.mochi.test/"], + }, + }); + await extension.startup(); + extension.sendMessage("test_fpi_disabled"); + await extension.awaitMessage("test_fpi_disabled"); + await enableFirstPartyIsolation(); + extension.sendMessage("test_fpi_enabled"); + await extension.awaitMessage("test_fpi_enabled"); + extension.sendMessage("test_fpi_with_invalid_characters"); + await extension.awaitMessage("test_fpi_with_invalid_characters"); + await disableFirstPartyIsolation(); + extension.sendMessage("test_fpd_cookies_on_fpi_disabled"); + await extension.awaitMessage("test_fpd_cookies_on_fpi_disabled"); + await extension.unload(); +}); +</script> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_incognito.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_incognito.html new file mode 100644 index 0000000000..a7c6931c06 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_incognito.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_cookies_incognito_not_allowed() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.allowPrivateBrowsingByDefault", false]], + }); + + let privateExtension = ExtensionTestUtils.loadExtension({ + incognitoOverride: "spanning", + async background() { + let window = await browser.windows.create({incognito: true}); + browser.test.onMessage.addListener(async () => { + await browser.windows.remove(window.id); + browser.test.sendMessage("done"); + }); + browser.test.sendMessage("ready"); + }, + manifest: { + permissions: ["cookies", "*://example.org/"], + }, + }); + await privateExtension.startup(); + await privateExtension.awaitMessage("ready"); + + async function background() { + const storeId = "firefox-private"; + const url = "http://example.org/"; + + // Getting the wrong storeId will fail, otherwise we should finish the test fine. + browser.cookies.onChanged.addListener(changeInfo => { + let {cookie} = changeInfo; + browser.test.assertTrue(cookie.storeId != storeId, "cookie store is correct"); + }); + + browser.test.onMessage.addListener(async () => { + let stores = await browser.cookies.getAllCookieStores(); + let store = stores.find(s => s.incognito); + browser.test.assertTrue(!store, "incognito cookie store should not be available"); + browser.test.notifyPass("cookies"); + }); + + await browser.test.assertRejects( + browser.cookies.set({url, name: "test", storeId}), + /Extension disallowed access/, + "API should reject setting cookie"); + await browser.test.assertRejects( + browser.cookies.get({url, name: "test", storeId}), + /Extension disallowed access/, + "API should reject getting cookie"); + await browser.test.assertRejects( + browser.cookies.getAll({url, storeId}), + /Extension disallowed access/, + "API should reject getting cookie"); + await browser.test.assertRejects( + browser.cookies.remove({url, name: "test", storeId}), + /Extension disallowed access/, + "API should reject getting cookie"); + await browser.test.assertRejects( + browser.cookies.getAll({url, storeId}), + /Extension disallowed access/, + "API should reject getting cookie"); + + browser.test.sendMessage("set-cookies"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["cookies", "*://example.org/"], + }, + }); + + await extension.startup(); + await extension.awaitMessage("set-cookies"); + + let chromeScript = SpecialPowers.loadChromeScript(() => { + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + + Services.cookies.add("example.org", "/", "public", `foo${Math.random()}`, + false, false, false, Number.MAX_SAFE_INTEGER, {}, + Ci.nsICookie.SAMESITE_NONE); + Services.cookies.add("example.org", "/", "private", `foo${Math.random()}`, + false, false, false, Number.MAX_SAFE_INTEGER, {privateBrowsingId: 1}, + Ci.nsICookie.SAMESITE_NONE); + }); + extension.sendMessage("test-cookie-store"); + await extension.awaitFinish("cookies"); + + await extension.unload(); + privateExtension.sendMessage("close"); + await privateExtension.awaitMessage("done"); + await privateExtension.unload(); + chromeScript.destroy(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html new file mode 100644 index 0000000000..0bd2852075 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html @@ -0,0 +1,115 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <script type="text/javascript" src="head_cookies.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function init() { + // We need to trigger a cookie eviction in order to test our batch delete + // observer. + + // Set quotaPerHost to maxPerHost - 1, so there is only one cookie + // will be evicted everytime. + SpecialPowers.setIntPref("network.cookie.quotaPerHost", 2); + SpecialPowers.setIntPref("network.cookie.maxPerHost", 3); + SimpleTest.registerCleanupFunction(() => { + SpecialPowers.clearUserPref("network.cookie.quotaPerHost"); + SpecialPowers.clearUserPref("network.cookie.maxPerHost"); + }); +}); + +add_task(async function test_bad_cookie_permissions() { + info("Test non-matching, non-secure domain with non-secure cookie"); + await testCookies({ + permissions: ["http://example.com/", "cookies"], + url: "http://example.net/", + domain: "example.net", + secure: false, + shouldPass: false, + shouldWrite: false, + }); + + info("Test non-matching, secure domain with non-secure cookie"); + await testCookies({ + permissions: ["https://example.com/", "cookies"], + url: "https://example.net/", + domain: "example.net", + secure: false, + shouldPass: false, + shouldWrite: false, + }); + + info("Test non-matching, secure domain with secure cookie"); + await testCookies({ + permissions: ["https://example.com/", "cookies"], + url: "https://example.net/", + domain: "example.net", + secure: false, + shouldPass: false, + shouldWrite: false, + }); + + info("Test matching subdomain with superdomain privileges, secure cookie (http)"); + await testCookies({ + permissions: ["http://foo.bar.example.com/", "cookies"], + url: "http://foo.bar.example.com/", + domain: ".example.com", + secure: true, + shouldPass: false, + shouldWrite: true, + }); + + info("Test matching, non-secure domain with secure cookie"); + await testCookies({ + permissions: ["http://example.com/", "cookies"], + url: "http://example.com/", + domain: "example.com", + secure: true, + shouldPass: false, + shouldWrite: true, + }); + + info("Test matching, non-secure host, secure URL"); + await testCookies({ + permissions: ["http://example.com/", "cookies"], + url: "https://example.com/", + domain: "example.com", + secure: true, + shouldPass: false, + shouldWrite: false, + }); + + info("Test non-matching domain"); + await testCookies({ + permissions: ["http://example.com/", "cookies"], + url: "http://example.com/", + domain: "example.net", + secure: false, + shouldPass: false, + shouldWrite: false, + }); + + info("Test invalid scheme"); + await testCookies({ + permissions: ["ftp://example.com/", "cookies"], + url: "ftp://example.com/", + domain: "example.com", + secure: false, + shouldPass: false, + shouldWrite: false, + }); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html new file mode 100644 index 0000000000..bd76f2b9c0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <script type="text/javascript" src="head_cookies.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function init() { + // We need to trigger a cookie eviction in order to test our batch delete + // observer. + + // Set quotaPerHost to maxPerHost - 1, so there is only one cookie + // will be evicted everytime. + SpecialPowers.setIntPref("network.cookie.quotaPerHost", 2); + SpecialPowers.setIntPref("network.cookie.maxPerHost", 3); + SimpleTest.registerCleanupFunction(() => { + SpecialPowers.clearUserPref("network.cookie.quotaPerHost"); + SpecialPowers.clearUserPref("network.cookie.maxPerHost"); + }); +}); + +add_task(async function test_good_cookie_permissions() { + info("Test matching, non-secure domain with non-secure cookie"); + await testCookies({ + permissions: ["http://example.com/", "cookies"], + url: "http://example.com/", + domain: "example.com", + secure: false, + shouldPass: true, + }); + + info("Test matching, secure domain with non-secure cookie"); + await testCookies({ + permissions: ["https://example.com/", "cookies"], + url: "https://example.com/", + domain: "example.com", + secure: false, + shouldPass: true, + }); + + info("Test matching, secure domain with secure cookie"); + await testCookies({ + permissions: ["https://example.com/", "cookies"], + url: "https://example.com/", + domain: "example.com", + secure: true, + shouldPass: true, + }); + + info("Test matching subdomain with superdomain privileges, secure cookie (https)"); + await testCookies({ + permissions: ["https://foo.bar.example.com/", "cookies"], + url: "https://foo.bar.example.com/", + domain: ".example.com", + secure: true, + shouldPass: true, + }); + + info("Test matching subdomain with superdomain privileges, non-secure cookie (https)"); + await testCookies({ + permissions: ["https://foo.bar.example.com/", "cookies"], + url: "https://foo.bar.example.com/", + domain: ".example.com", + secure: false, + shouldPass: true, + }); + + info("Test matching subdomain with superdomain privileges, non-secure cookie (http)"); + await testCookies({ + permissions: ["http://foo.bar.example.com/", "cookies"], + url: "http://foo.bar.example.com/", + domain: ".example.com", + secure: false, + shouldPass: true, + }); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_downloads_download.html b/toolkit/components/extensions/test/mochitest/test_ext_downloads_download.html new file mode 100644 index 0000000000..ea163db0de --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_downloads_download.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Downloads Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +async function background() { + const url = "http://mochi.test:8888/tests/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_page.html"; + + browser.test.assertThrows( + () => browser.downloads.download(), + /Incorrect argument types for downloads.download/, + "Should fail without options" + ); + + browser.test.assertThrows( + () => browser.downloads.download({url: "invalid url"}), + /invalid url is not a valid URL/, + "Should fail on invalid URL" + ); + + browser.test.assertThrows( + () => browser.downloads.download({}), + /Property "url" is required/, + "Should fail with no URL" + ); + + browser.test.assertThrows( + () => browser.downloads.download({url, method: "DELETE"}), + /Invalid enumeration value "DELETE"/, + "Should fail with invalid method" + ); + + await browser.test.assertRejects( + browser.downloads.download({url, headers: [{name: "Host", value: "Banana"}]}), + /Forbidden request header name/, + "Should fail with a forbidden header" + ); + + await browser.test.assertRejects( + browser.downloads.download({url, filename: "/tmp/file.gif"}), + /filename must not be an absolute path/, + "Should fail with an absolute file path" + ); + + await browser.test.assertRejects( + browser.downloads.download({url, filename: ""}), + /filename must not be empty/, + "Should fail with an empty file path" + ); + + await browser.test.assertRejects( + browser.downloads.download({url, filename: "file."}), + /filename must not contain illegal characters/, + "Should fail with a dot in the filename" + ); + + await browser.test.assertRejects( + browser.downloads.download({url, filename: "../file.gif"}), + /filename must not contain back-references/, + "Should fail with a file path that contains back-references" + ); + + browser.test.notifyPass("download.done"); +} + +add_task(async function test_invalid_download_parameters() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: {permissions: ["downloads"]}, + background, + }); + await extension.startup(); + + await extension.awaitFinish("download.done"); + + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_embeddedimg_iframe_frameAncestors.html b/toolkit/components/extensions/test/mochitest/test_ext_embeddedimg_iframe_frameAncestors.html new file mode 100644 index 0000000000..d6702da4d3 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_embeddedimg_iframe_frameAncestors.html @@ -0,0 +1,94 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test checking webRequest.onBeforeRequest details object</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +let expected = { + "file_contains_iframe.html": { + type: "main_frame", + frameAncestor_length: 0, + }, + "file_contains_img.html": { + type: "sub_frame", + frameAncestor_length: 1, + }, + "file_image_good.png": { + type: "image", + frameAncestor_length: 1, + } +}; + +function checkDetails(details) { + let url = new URL(details.url); + let filename = url.pathname.split("/").pop(); + ok(expected.hasOwnProperty(filename), `Should be expecting a request for ${filename}`); + let expect = expected[filename]; + is(expect.type, details.type, `${details.type} type matches`); + is(expect.frameAncestor_length, details.frameAncestors.length, "incorrect frameAncestors length"); + if (filename == "file_contains_img.html") { + is(details.frameAncestors[0].frameId, details.parentFrameId, + "frameAncestors[0] should match parentFrameId"); + expected["file_image_good.png"].frameId = details.frameId; + } else if (filename == "file_image_good.png") { + is(details.frameAncestors[0].frameId, details.parentFrameId, + "frameAncestors[0] should match parentFrameId"); + is(details.frameId, expect.frameId, + "frameId for image and iframe should match"); + } +} + +add_task(async () => { + // Clear the image cache, since it gets in the way otherwise. + let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools); + let cache = imgTools.getImgCacheForDocument(document); + cache.clearCache(false); + await SpecialPowers.spawnChrome([], async () => { + Services.cache2.clear(); + }); + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "<all_urls>"], + }, + background() { + browser.webRequest.onBeforeRequest.addListener( + details => { + browser.test.sendMessage("onBeforeRequest", details); + }, + { + urls: [ + "http://example.org/*/file_contains_img.html", + "http://mochi.test/*/file_contains_iframe.html", + "*://*/*.png", + ], + } + ); + }, + }); + + await extension.startup(); + const FILE_URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html"; + let win = window.open(FILE_URL); + await new Promise(resolve => win.addEventListener("load", () => resolve(), {once: true})); + + for (let i = 0; i < Object.keys(expected).length; i++) { + checkDetails(await extension.awaitMessage("onBeforeRequest")); + } + + win.close(); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html b/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html new file mode 100644 index 0000000000..bdf300ec50 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_contentscript() { + function background() { + browser.runtime.onMessage.addListener(([script], sender) => { + browser.test.sendMessage("run", {script}); + browser.test.sendMessage("run-" + script); + }); + browser.test.sendMessage("running"); + } + + function contentScriptAll() { + browser.runtime.sendMessage(["all"]); + } + function contentScriptIncludesTest1() { + browser.runtime.sendMessage(["includes-test1"]); + } + function contentScriptExcludesTest1() { + browser.runtime.sendMessage(["excludes-test1"]); + } + + let extensionData = { + manifest: { + content_scripts: [ + { + "matches": ["http://example.org/", "http://*.example.org/"], + "exclude_globs": [], + "include_globs": ["*"], + "js": ["content_script_all.js"], + }, + { + "matches": ["http://example.org/", "http://*.example.org/"], + "include_globs": ["*test1*"], + "js": ["content_script_includes_test1.js"], + }, + { + "matches": ["http://example.org/", "http://*.example.org/"], + "exclude_globs": ["*test1*"], + "js": ["content_script_excludes_test1.js"], + }, + ], + }, + background, + + files: { + "content_script_all.js": contentScriptAll, + "content_script_includes_test1.js": contentScriptIncludesTest1, + "content_script_excludes_test1.js": contentScriptExcludesTest1, + }, + + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + let ran = 0; + extension.onMessage("run", ({script}) => { + ran++; + }); + + await Promise.all([extension.startup(), extension.awaitMessage("running")]); + info("extension loaded"); + + let win = window.open("http://example.org/"); + await Promise.all([extension.awaitMessage("run-all"), extension.awaitMessage("run-excludes-test1")]); + win.close(); + is(ran, 2); + + win = window.open("http://test1.example.org/"); + await Promise.all([extension.awaitMessage("run-all"), extension.awaitMessage("run-includes-test1")]); + win.close(); + is(ran, 4); + + await extension.unload(); + info("extension unloaded"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html b/toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html new file mode 100644 index 0000000000..ba91989d9c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html @@ -0,0 +1,110 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension external messaging</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function backgroundScript(id, otherId) { + browser.runtime.onMessage.addListener((msg, sender) => { + browser.test.fail(`Got unexpected message: ${uneval(msg)} ${uneval(sender)}`); + }); + + browser.runtime.onConnect.addListener(port => { + browser.test.fail(`Got unexpected connection: ${uneval(port.sender)}`); + }); + + browser.runtime.onMessageExternal.addListener((msg, sender) => { + browser.test.assertEq(otherId, sender.id, `${id}: Got expected external sender ID`); + browser.test.assertEq(`helo-${id}`, msg, "Got expected message"); + + browser.test.sendMessage("onMessage-done"); + + return Promise.resolve(`ehlo-${otherId}`); + }); + + browser.runtime.onConnectExternal.addListener(port => { + browser.test.assertEq(otherId, port.sender.id, `${id}: Got expected external connecter ID`); + + port.onMessage.addListener(msg => { + browser.test.assertEq(`helo-${id}`, msg, "Got expected port message"); + + port.postMessage(`ehlo-${otherId}`); + + browser.test.sendMessage("onConnect-done"); + }); + }); + + browser.test.onMessage.addListener(msg => { + if (msg === "go") { + browser.runtime.sendMessage(otherId, `helo-${otherId}`).then(result => { + browser.test.assertEq(`ehlo-${id}`, result, "Got expected reply"); + browser.test.sendMessage("sendMessage-done"); + }); + + let port = browser.runtime.connect(otherId); + port.postMessage(`helo-${otherId}`); + + port.onMessage.addListener(msg => { + port.disconnect(); + + browser.test.assertEq(msg, `ehlo-${id}`, "Got expected port reply"); + browser.test.sendMessage("connect-done"); + }); + } + }); +} + +function makeExtension(id, otherId) { + let args = `${JSON.stringify(id)}, ${JSON.stringify(otherId)}`; + + let extensionData = { + background: `(${backgroundScript})(${args})`, + manifest: { + "applications": {"gecko": {id}}, + }, + }; + + return ExtensionTestUtils.loadExtension(extensionData); +} + +add_task(async function test_contentscript() { + const ID1 = "foo-message@mochitest.mozilla.org"; + const ID2 = "bar-message@mochitest.mozilla.org"; + + let extension1 = makeExtension(ID1, ID2); + let extension2 = makeExtension(ID2, ID1); + + await Promise.all([extension1.startup(), extension2.startup()]); + + extension1.sendMessage("go"); + extension2.sendMessage("go"); + + await Promise.all([ + extension1.awaitMessage("sendMessage-done"), + extension2.awaitMessage("sendMessage-done"), + + extension1.awaitMessage("onMessage-done"), + extension2.awaitMessage("onMessage-done"), + + extension1.awaitMessage("connect-done"), + extension2.awaitMessage("connect-done"), + + extension1.awaitMessage("onConnect-done"), + extension2.awaitMessage("onConnect-done"), + ]); + + await extension1.unload(); + await extension2.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_generate.html b/toolkit/components/extensions/test/mochitest/test_ext_generate.html new file mode 100644 index 0000000000..ba88d16ca3 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_generate.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for generating WebExtensions</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function background() { + browser.test.log("running background script"); + + browser.test.onMessage.addListener((x, y) => { + browser.test.assertEq(x, 10, "x is 10"); + browser.test.assertEq(y, 20, "y is 20"); + + browser.test.notifyPass("background test passed"); + }); + + browser.test.sendMessage("running", 1); +} + +let extensionData = { + background, +}; + +add_task(async function test_background() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + info("load complete"); + let [, x] = await Promise.all([extension.startup(), extension.awaitMessage("running")]); + is(x, 1, "got correct value from extension"); + info("startup complete"); + extension.sendMessage(10, 20); + await extension.awaitFinish(); + info("test complete"); + await extension.unload(); + info("extension unloaded successfully"); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_geolocation.html b/toolkit/components/extensions/test/mochitest/test_ext_geolocation.html new file mode 100644 index 0000000000..9f326372bb --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_geolocation.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +add_task(async function test_geolocation_nopermission() { + let GEO_URL = "http://mochi.test:8888/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs"; + await SpecialPowers.pushPrefEnv({"set": [["geo.provider.network.url", GEO_URL]]}); +}); + +add_task(async function test_geolocation() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "geolocation", + ], + }, + background() { + navigator.geolocation.getCurrentPosition(() => { + browser.test.notifyPass("success geolocation call"); + }, (error) => { + browser.test.notifyFail(`geolocation call ${error}`); + }); + }, + }); + + await extension.startup(); + await extension.awaitFinish(); + await extension.unload(); +}); + +add_task(async function test_geolocation_nopermission() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + navigator.geolocation.getCurrentPosition(() => { + browser.test.notifyFail("success geolocation call"); + }, (error) => { + browser.test.notifyPass(`geolocation call ${error}`); + }); + }, + }); + + await extension.startup(); + await extension.awaitFinish(); + await extension.unload(); +}); + +add_task(async function test_geolocation_prompt() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.tabs.create({url: "tab.html"}); + }, + files: { + "tab.html": `<html><head> + <meta charset="utf-8"> + <script src="tab.js"><\/script> + </head></html>`, + "tab.js": () => { + navigator.geolocation.getCurrentPosition(() => { + browser.test.notifyPass("success geolocation call"); + }, (error) => { + browser.test.notifyFail(`geolocation call ${error}`); + }); + }, + }, + }); + + // Bypass the actual prompt, but the prompt result is to allow access. + await SpecialPowers.pushPrefEnv({"set": [["geo.prompt.testing", true], ["geo.prompt.testing.allow", true]]}); + await extension.startup(); + await extension.awaitFinish(); + await extension.unload(); +}); +</script> +</head> +<body> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_identity.html b/toolkit/components/extensions/test/mochitest/test_ext_identity.html new file mode 100644 index 0000000000..c40578cd40 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_identity.html @@ -0,0 +1,390 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebExtension Identity</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.webextensions.identity.redirectDomain", "example.com"], + // Disable the network cache first-party partition during this + // test (TODO: look more closely to how that is affecting the intermittency + // of this test on MacOS, see Bug 1626482). + ["privacy.partition.network_state", false], + ], + }); +}); + +add_task(async function test_noPermission() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.test.assertEq( + undefined, + browser.identity, + "No identity api without permission" + ); + browser.test.sendMessage("done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function test_getRedirectURL() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: "identity@mozilla.org", + }, + }, + permissions: ["identity", "https://example.com/"], + }, + async background() { + let redirect_base = + "https://35b64b676900f491c00e7f618d43f7040e88422e.example.com/"; + await browser.test.assertEq( + redirect_base, + browser.identity.getRedirectURL(), + "redirect url ok" + ); + await browser.test.assertEq( + redirect_base, + browser.identity.getRedirectURL(""), + "redirect url ok" + ); + await browser.test.assertEq( + redirect_base + "foobar", + browser.identity.getRedirectURL("foobar"), + "redirect url ok" + ); + await browser.test.assertEq( + redirect_base + "callback", + browser.identity.getRedirectURL("/callback"), + "redirect url ok" + ); + browser.test.sendMessage("done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function test_badAuthURI() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["identity", "https://example.com/"], + }, + async background() { + for (let url of [ + "foobar", + "about:addons", + "about:blank", + "ftp://example.com/test", + ]) { + await browser.test.assertThrows( + () => { + browser.identity.launchWebAuthFlow({ interactive: true, url }); + }, + /Type error for parameter details/, + "details.url is invalid" + ); + } + + browser.test.sendMessage("done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function test_badRequestURI() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["identity", "https://example.com/"], + }, + async background() { + let base_uri = + "https://example.com/tests/toolkit/components/extensions/test/mochitest/"; + let url = `${base_uri}?redirect_uri=badrobot}`; + await browser.test.assertRejects( + browser.identity.launchWebAuthFlow({ interactive: true, url }), + "redirect_uri is invalid", + "invalid redirect url" + ); + url = `${base_uri}?redirect_uri=https://somesite.com`; + await browser.test.assertRejects( + browser.identity.launchWebAuthFlow({ interactive: true, url }), + "redirect_uri not allowed", + "invalid redirect url" + ); + browser.test.sendMessage("done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function background_launchWebAuthFlow_requires_interaction() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["identity", "https://example.com/"], + }, + async background() { + let base_uri = + "https://example.com/tests/toolkit/components/extensions/test/mochitest/"; + let url = `${base_uri}?redirect_uri=${browser.identity.getRedirectURL( + "redirect" + )}`; + await browser.test.assertRejects( + browser.identity.launchWebAuthFlow({ interactive: false, url }), + "Requires user interaction", + "Rejects on required user interaction" + ); + browser.test.sendMessage("done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +function background_launchWebAuthFlow({ + interactive = false, + path = "redirect_auto.sjs", + params = {}, + redirect = true, + useRedirectUri = true, +} = {}) { + let uri_path = useRedirectUri ? "identity_cb" : ""; + let expected_redirect = `https://35b64b676900f491c00e7f618d43f7040e88422e.example.com/${uri_path}`; + let base_uri = + "https://example.com/tests/toolkit/components/extensions/test/mochitest/"; + let redirect_uri = browser.identity.getRedirectURL( + useRedirectUri ? uri_path : undefined + ); + browser.test.assertEq( + expected_redirect, + redirect_uri, + "expected redirect uri matches hash" + ); + let url = `${base_uri}${path}`; + if (useRedirectUri) { + params.redirect_uri = redirect_uri; + } else { + // We kind of fake it with the redirect url that would normally be configured + // in the oauth service. This does still test that the identity service falls back + // to the extensions redirect url. + params.default_redirect = expected_redirect; + } + if (!redirect) { + params.no_redirect = 1; + } + let query = []; + for (let [param, value] of Object.entries(params)) { + query.push(`${param}=${encodeURIComponent(value)}`); + } + url = `${url}?${query.join("&")}`; + + // Ensure we do not start the actual request for the redirect url. In the case + // of a 303 POST redirect we are getting a request started. + let watchRedirectRequest = () => {}; + if (params.post !== 303) { + watchRedirectRequest = details => { + if (details.url.startsWith(expected_redirect)) { + browser.test.fail(`onBeforeRequest called for redirect url: ${JSON.stringify(details)}`); + } + }; + + browser.webRequest.onBeforeRequest.addListener( + watchRedirectRequest, + { + urls: [ + "https://35b64b676900f491c00e7f618d43f7040e88422e.example.com/*", + ], + } + ); + } + + browser.identity + .launchWebAuthFlow({ interactive, url }) + .then(redirectURL => { + browser.test.assertTrue( + redirectURL.startsWith(redirect_uri), + `correct redirect url ${redirectURL}` + ); + if (redirect) { + let url = new URL(redirectURL); + browser.test.assertEq( + "here ya go", + url.searchParams.get("access_token"), + "Handled auto redirection" + ); + } + }) + .catch(error => { + if (redirect) { + browser.test.fail(error.message); + } else { + browser.test.assertEq( + "Requires user interaction", + error.message, + "Auth page loaded, interaction required." + ); + } + }).then(() => { + browser.webRequest.onBeforeRequest.removeListener(watchRedirectRequest); + browser.test.sendMessage("done"); + }); +} + +// Tests the situation where the oauth provider has already granted access and +// simply redirects the oauth client to provide the access key or code. +add_task(async function test_autoRedirect() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: "identity@mozilla.org", + }, + }, + permissions: ["webRequest", "identity", "https://*.example.com/*"], + }, + background: `(${background_launchWebAuthFlow})()`, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function test_autoRedirect_noRedirectURI() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: "identity@mozilla.org", + }, + }, + permissions: ["webRequest", "identity", "https://*.example.com/*"], + }, + background: `(${background_launchWebAuthFlow})({useRedirectUri: false})`, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +// Tests the situation where the oauth provider has not granted access and interactive=false +add_task(async function test_noRedirect() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: "identity@mozilla.org", + }, + }, + permissions: ["webRequest", "identity", "https://*.example.com/*"], + }, + background: `(${background_launchWebAuthFlow})({redirect: false})`, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +// Tests the situation where the oauth provider must show a window where +// presumably the user interacts, then the redirect occurs and access key or +// code is provided. We bypass any real interaction, but want the window to +// open and result in a redirect. +add_task(async function test_interaction() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: "identity@mozilla.org", + }, + }, + permissions: ["webRequest", "identity", "https://*.example.com/*"], + }, + background: `(${background_launchWebAuthFlow})({interactive: true, path: "oauth.html"})`, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +// Tests the situation where the oauth provider redirects with a 303. +add_task(async function test_auto303Redirect() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: "identity@mozilla.org", + }, + }, + permissions: ["webRequest", "identity", "https://*.example.com/*"], + }, + background: `(${background_launchWebAuthFlow})({interactive: true, path: "oauth.html", params: {post: 303, server_uri: "redirect_auto.sjs"}})`, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function test_loopbackRedirectURI() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: "identity@mozilla.org", + }, + }, + permissions: ["identity"], + }, + async background() { + let redirectURL = "http://127.0.0.1/mozoauth2/35b64b676900f491c00e7f618d43f7040e88422e"; + let actualRedirect = await browser.identity.launchWebAuthFlow({ + interactive: true, + url: `https://example.com/tests/toolkit/components/extensions/test/mochitest/oauth.html?redirect_uri=${encodeURIComponent(redirectURL)}` + }).catch(error => { + browser.test.fail(error.message) + }); + browser.test.assertTrue( + actualRedirect.startsWith(redirectURL), + "Expected redirect url to be loopback address" + ) + browser.test.sendMessage("done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_idle.html b/toolkit/components/extensions/test/mochitest/test_ext_idle.html new file mode 100644 index 0000000000..381687ee38 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_idle.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function testWithRealIdleService() { + function background() { + browser.test.onMessage.addListener(async (msg, ...args) => { + let detectionInterval = args[0]; + if (msg == "addListener") { + let status = await browser.idle.queryState(detectionInterval); + browser.test.assertEq("active", status, "Idle status is active"); + browser.idle.setDetectionInterval(detectionInterval); + browser.idle.onStateChanged.addListener(newState => { + browser.test.assertEq("idle", newState, "listener fired with the expected state"); + browser.test.sendMessage("listenerFired"); + }); + browser.test.sendMessage("listenerAdded"); + } else if (msg == "checkState") { + let status = await browser.idle.queryState(detectionInterval); + browser.test.assertEq("idle", status, "Idle status is idle"); + browser.test.notifyPass("idle"); + } + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["idle"], + }, + }); + + await extension.startup(); + + let chromeScript = loadChromeScript(() => { + const {sendAsyncMessage} = this; + const idleService = Cc["@mozilla.org/widget/useridleservice;1"].getService(Ci.nsIUserIdleService); + let idleTime = idleService.idleTime; + sendAsyncMessage("detectionInterval", Math.max(Math.ceil(idleTime / 1000) + 10, 15)); + }); + let detectionInterval = await chromeScript.promiseOneMessage("detectionInterval"); + chromeScript.destroy(); + + info(`Setting interval to ${detectionInterval}`); + extension.sendMessage("addListener", detectionInterval); + await extension.awaitMessage("listenerAdded"); + info("Listener added"); + await extension.awaitMessage("listenerFired"); + info("Listener fired"); + extension.sendMessage("checkState", detectionInterval); + await extension.awaitFinish("idle"); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html b/toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html new file mode 100644 index 0000000000..5b36902581 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_in_incognito_context_true() { + function background() { + browser.runtime.onMessage.addListener(msg => { + browser.test.assertEq(true, msg, "inIncognitoContext is true"); + browser.test.notifyPass("inIncognitoContext"); + }); + + browser.windows.create({url: browser.runtime.getURL("/tab.html"), incognito: true}); + } + + function tabScript() { + browser.runtime.sendMessage(browser.extension.inIncognitoContext); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + files: { + "tab.js": tabScript, + "tab.html": `<!DOCTYPE html><html><head> + <meta charset="utf-8"> + <script src="tab.js"><\/script> + </head></html>`, + }, + incognitoOverride: "spanning", + }); + + await extension.startup(); + await extension.awaitFinish("inIncognitoContext"); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html b/toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html new file mode 100644 index 0000000000..cc161f735f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_listener_proxies() { + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + + manifest: { + "permissions": ["storage"], + }, + + async background() { + // Test that adding multiple listeners for the same event works as + // expected. + + let awaitChanged = () => new Promise(resolve => { + browser.storage.onChanged.addListener(function listener() { + browser.storage.onChanged.removeListener(listener); + resolve(); + }); + }); + + let promises = [ + awaitChanged(), + awaitChanged(), + ]; + + function removedListener() {} + browser.storage.onChanged.addListener(removedListener); + browser.storage.onChanged.removeListener(removedListener); + + promises.push(awaitChanged(), awaitChanged()); + + browser.storage.local.set({foo: "bar"}); + + await Promise.all(promises); + + browser.test.notifyPass("onchanged-listeners"); + }, + }); + + await extension.startup(); + + await extension.awaitFinish("onchanged-listeners"); + + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_new_tab_processType.html b/toolkit/components/extensions/test/mochitest/test_ext_new_tab_processType.html new file mode 100644 index 0000000000..4561ef1a28 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_new_tab_processType.html @@ -0,0 +1,152 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for opening links in new tabs from extension frames</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function promiseObserved(topic, check) { + return new Promise(resolve => { + let obs = SpecialPowers.Services.obs; + + function observer(subject, topic, data) { + subject = SpecialPowers.wrap(subject); + if (check(subject, data)) { + obs.removeObserver(observer, topic); + resolve({subject, data}); + } + } + obs.addObserver(observer, topic); + }); +} + +add_task(async function test_target_blank_link_no_opener_from_privileged() { + const linkURL = "http://example.com/"; + + function extension_tab() { + document.getElementById("link").click(); + } + + function content_script() { + browser.runtime.sendMessage("content_page_loaded"); + } + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + applications: { gecko: { id: "target_blank_link@tests.mozilla.org" } }, + content_scripts: [{ + js: ["content_script.js"], + matches: ["http://example.com/*"], + run_at: "document_idle", + }], + permissions: ["tabs"], + }, + files: { + "page.html": `<!DOCTYPE html> + <html> + <head><meta charset="utf-8"></html> + <body> + <a href="${linkURL}" target="_blank" id="link">link</a> + <script src="extension_tab.js"><\/script> + </body> + </html>`, + "extension_tab.js": extension_tab, + "content_script.js": content_script, + }, + background() { + let pageTab; + browser.runtime.onMessage.addListener((msg, sender) => { + if (sender.tab) { + browser.test.sendMessage(msg, sender.tab.url); + browser.tabs.remove(sender.tab.id); + browser.tabs.remove(pageTab.id); + } + }); + pageTab = browser.tabs.create({ url: browser.runtime.getURL("page.html") }); + }, + }); + + await extension.startup(); + + // Make sure page is loaded correctly + const url = await extension.awaitMessage("content_page_loaded"); + is(url, linkURL, "Page URL should match"); + + await extension.unload(); +}); + +add_task(async function test_target_blank_link() { + const linkURL = "http://mochi.test:8888/tests/toolkit/"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_security_policy: "script-src 'self' 'unsafe-eval'; object-src 'self';", + + web_accessible_resources: ["iframe.html"], + }, + files: { + "iframe.html": `<!DOCTYPE html> + <html> + <head><meta charset="utf-8"></html> + <body> + <a href="${linkURL}" target="_blank" id="link" rel="opener">link</a> + </body> + </html>`, + }, + background() { + browser.test.sendMessage("frame_url", browser.runtime.getURL("iframe.html")); + }, + }); + + await extension.startup(); + + let url = await extension.awaitMessage("frame_url"); + + let iframe = document.createElement("iframe"); + iframe.src = url; + document.body.appendChild(iframe); + await new Promise(resolve => iframe.addEventListener("load", () => setTimeout(resolve, 0), {once: true})); + + let win = SpecialPowers.wrap(iframe).contentWindow; + + { + // Flush layout so that synthesizeMouseAtCenter on a cross-origin iframe + // works as expected. + document.body.getBoundingClientRect(); + + let promise = promiseObserved("document-element-inserted", doc => doc.documentURI === linkURL); + + await SpecialPowers.spawn(iframe, [], async () => { + this.content.document.getElementById("link").click(); + }); + + let {subject: doc} = await promise; + info("Link opened"); + doc.defaultView.close(); + info("Window closed"); + } + + { + let promise = promiseObserved("document-element-inserted", doc => doc.documentURI === linkURL); + + let res = win.eval(`window.open("${linkURL}")`); + let {subject: doc} = await promise; + is(SpecialPowers.unwrap(res), SpecialPowers.unwrap(doc.defaultView), "window.open worked as expected"); + + doc.defaultView.close(); + } + + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_notifications.html b/toolkit/components/extensions/test/mochitest/test_ext_notifications.html new file mode 100644 index 0000000000..7a91320373 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_notifications.html @@ -0,0 +1,340 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for notifications</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head_notifications.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +// A 1x1 PNG image. +// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain) +let image = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="); +const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer; + +add_task(async function setup_mock_alert_service() { + await MockAlertsService.register(); +}); + +add_task(async function test_notification() { + async function background() { + let opts = { + type: "basic", + title: "Testing Notification", + message: "Carry on", + }; + + let id = await browser.notifications.create(opts); + + browser.test.sendMessage("running", id); + browser.test.notifyPass("background test passed"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["notifications"], + }, + background, + }); + await extension.startup(); + let x = await extension.awaitMessage("running"); + is(x, "0", "got correct id from notifications.create"); + await extension.awaitFinish(); + await extension.unload(); +}); + +add_task(async function test_notification_events() { + async function background() { + let opts = { + type: "basic", + title: "Testing Notification", + message: "Carry on", + }; + + let createdId = "98"; + + // Test an ignored listener. + browser.notifications.onButtonClicked.addListener(function() {}); + + // We cannot test onClicked listener without a mock + // but we can attempt to add a listener. + browser.notifications.onClicked.addListener(async function(id) { + browser.test.assertEq(createdId, id, "onClicked has the expected ID"); + browser.test.sendMessage("notification-event", "clicked"); + }); + + browser.notifications.onShown.addListener(async function listener(id) { + browser.test.assertEq(createdId, id, "onShown has the expected ID"); + browser.test.sendMessage("notification-event", "shown"); + }); + + browser.test.onMessage.addListener(async function(msg, expectedCount) { + if (msg === "create-again") { + let newId = await browser.notifications.create(createdId, opts); + browser.test.assertEq(createdId, newId, "create returned the expected id."); + browser.test.sendMessage("notification-created-twice"); + } else if (msg === "check-count") { + let notifications = await browser.notifications.getAll(); + let ids = Object.keys(notifications); + browser.test.assertEq(expectedCount, ids.length, `getAll() = ${ids}`); + browser.test.sendMessage("check-count-result"); + } + }); + + // Test onClosed listener. + browser.notifications.onClosed.addListener(function listener(id) { + browser.test.assertEq(createdId, id, "onClosed received the expected id."); + browser.test.sendMessage("notification-event", "closed"); + }); + + await browser.notifications.create(createdId, opts); + + browser.test.sendMessage("notification-created-once"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["notifications"], + }, + background, + }); + + await extension.startup(); + + async function waitForNotificationEvent(name) { + info(`Waiting for notification event: ${name}`); + is(name, await extension.awaitMessage("notification-event"), + "Expected notification event"); + } + async function checkNotificationCount(expectedCount) { + extension.sendMessage("check-count", expectedCount); + await extension.awaitMessage("check-count-result"); + } + + await extension.awaitMessage("notification-created-once"); + await waitForNotificationEvent("shown"); + await checkNotificationCount(1); + + // On most platforms, clicking the notification closes it. + // But on macOS, the notification can repeatedly be clicked without closing. + await MockAlertsService.clickNotificationsWithoutClose(); + await waitForNotificationEvent("clicked"); + await checkNotificationCount(1); + await MockAlertsService.clickNotificationsWithoutClose(); + await waitForNotificationEvent("clicked"); + await checkNotificationCount(1); + await MockAlertsService.clickNotifications(); + await waitForNotificationEvent("clicked"); + await waitForNotificationEvent("closed"); + await checkNotificationCount(0); + + extension.sendMessage("create-again"); + await extension.awaitMessage("notification-created-twice"); + await waitForNotificationEvent("shown"); + await checkNotificationCount(1); + + await MockAlertsService.closeNotifications(); + await waitForNotificationEvent("closed"); + await checkNotificationCount(0); + + await extension.unload(); +}); + +add_task(async function test_notification_clear() { + function background() { + let opts = { + type: "basic", + title: "Testing Notification", + message: "Carry on", + }; + + let createdId = "99"; + + browser.notifications.onShown.addListener(async id => { + browser.test.assertEq(createdId, id, "onShown received the expected id."); + let wasCleared = await browser.notifications.clear(id); + browser.test.assertTrue(wasCleared, "notifications.clear returned true."); + }); + + browser.notifications.onClosed.addListener(id => { + browser.test.assertEq(createdId, id, "onClosed received the expected id."); + browser.test.notifyPass("background test passed"); + }); + + browser.notifications.create(createdId, opts); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["notifications"], + }, + background, + }); + + await extension.startup(); + await extension.awaitFinish(); + await extension.unload(); +}); + +add_task(async function test_notifications_empty_getAll() { + async function background() { + let notifications = await browser.notifications.getAll(); + + browser.test.assertEq("object", typeof notifications, "getAll() returned an object"); + browser.test.assertEq(0, Object.keys(notifications).length, "the object has no properties"); + browser.test.notifyPass("getAll empty"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["notifications"], + }, + background, + }); + await extension.startup(); + await extension.awaitFinish("getAll empty"); + await extension.unload(); +}); + +add_task(async function test_notifications_populated_getAll() { + async function background() { + let opts = { + type: "basic", + iconUrl: "a.png", + title: "Testing Notification", + message: "Carry on", + }; + + await browser.notifications.create("p1", opts); + await browser.notifications.create("p2", opts); + let notifications = await browser.notifications.getAll(); + + browser.test.assertEq("object", typeof notifications, "getAll() returned an object"); + browser.test.assertEq(2, Object.keys(notifications).length, "the object has 2 properties"); + + for (let notificationId of ["p1", "p2"]) { + for (let key of Object.keys(opts)) { + browser.test.assertEq( + opts[key], + notifications[notificationId][key], + `the notification has the expected value for option: ${key}` + ); + } + } + + browser.test.notifyPass("getAll populated"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["notifications"], + }, + background, + files: { + "a.png": IMAGE_ARRAYBUFFER, + }, + }); + await extension.startup(); + await extension.awaitFinish("getAll populated"); + await extension.unload(); +}); + +add_task(async function test_buttons_unsupported() { + function background() { + let opts = { + type: "basic", + title: "Testing Notification", + message: "Carry on", + buttons: [{title: "Button title"}], + }; + + let exception = {}; + try { + browser.notifications.create(opts); + } catch (e) { + exception = e; + } + + browser.test.assertTrue( + String(exception).includes('Property "buttons" is unsupported by Firefox'), + "notifications.create with buttons option threw an expected exception" + ); + browser.test.notifyPass("buttons-unsupported"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["notifications"], + }, + background, + }); + await extension.startup(); + await extension.awaitFinish("buttons-unsupported"); + await extension.unload(); +}); + +add_task(async function test_notifications_different_contexts() { + async function background() { + let opts = { + type: "basic", + title: "Testing Notification", + message: "Carry on", + }; + + let id = await browser.notifications.create(opts); + + browser.runtime.onMessage.addListener(async (message, sender) => { + await browser.tabs.remove(sender.tab.id); + + // We should be able to clear the notification after creating and + // destroying the tab.html page. + let wasCleared = await browser.notifications.clear(id); + browser.test.assertTrue(wasCleared, "The notification was cleared."); + browser.test.notifyPass("notifications"); + }); + + browser.tabs.create({url: browser.runtime.getURL("/tab.html")}); + } + + async function tabScript() { + // We should be able to see the notification created in the background page + // in this page. + let notifications = await browser.notifications.getAll(); + browser.test.assertEq(1, Object.keys(notifications).length, + "One notification found."); + browser.runtime.sendMessage("continue-test"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["notifications"], + }, + background, + files: { + "tab.js": tabScript, + "tab.html": `<!DOCTYPE html><html><head> + <meta charset="utf-8"> + <script src="tab.js"><\/script> + </head></html>`, + }, + }); + + await extension.startup(); + await extension.awaitFinish("notifications"); + await extension.unload(); +}); + +add_task(async function teardown_mock_alert_service() { + await MockAlertsService.unregister(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html b/toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html new file mode 100644 index 0000000000..10305c8ac0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html @@ -0,0 +1,394 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for protocol handlers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +/* eslint-disable mozilla/balanced-listeners */ +/* global addMessageListener, sendAsyncMessage */ + +function protocolChromeScript() { + addMessageListener("setup", () => { + let data = {}; + const protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService); + let protoInfo = protoSvc.getProtocolHandlerInfo("ext+foo"); + data.preferredAction = protoInfo.preferredAction === protoInfo.useHelperApp; + + let handlers = protoInfo.possibleApplicationHandlers; + data.handlers = handlers.length; + + let handler = handlers.queryElementAt(0, Ci.nsIHandlerApp); + data.isWebHandler = handler instanceof Ci.nsIWebHandlerApp; + data.uriTemplate = handler.uriTemplate; + + // ext+ protocols should be set as default when there is only one + data.preferredApplicationHandler = protoInfo.preferredApplicationHandler == handler; + data.alwaysAskBeforeHandling = protoInfo.alwaysAskBeforeHandling; + const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"] + .getService(Ci.nsIHandlerService); + handlerSvc.store(protoInfo); + + sendAsyncMessage("handlerData", data); + }); +} + +add_task(async function test_protocolHandler() { + await SpecialPowers.pushPrefEnv({set: [ + ["extensions.allowPrivateBrowsingByDefault", false], + // Disabling the external protocol permission prompt. We don't need it + // for this test. + ["security.external_protocol_requires_permission", false], + ]}); + let extensionData = { + manifest: { + "protocol_handlers": [ + { + "protocol": "ext+foo", + "name": "a foo protocol handler", + "uriTemplate": "foo.html?val=%s", + }, + ], + }, + + background() { + browser.test.onMessage.addListener(async (msg, arg) => { + if (msg == "open") { + let tab = await browser.tabs.create({url: arg}); + browser.test.sendMessage("opened", tab.id); + } else if (msg == "close") { + await browser.tabs.remove(arg); + browser.test.sendMessage("closed"); + } + }); + browser.test.sendMessage("test-url", browser.runtime.getURL("foo.html")); + }, + + files: { + "foo.js": function() { + browser.test.sendMessage("test-query", location.search); + }, + "foo.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <script src="foo.js"><\/script> + </head> + </html>`, + }, + }; + + let pb_extension = ExtensionTestUtils.loadExtension({ + background() { + browser.test.onMessage.addListener(async (msg, arg) => { + if (msg == "open") { + let win = await browser.windows.create({ url: arg, incognito: true }); + browser.test.sendMessage("opened", { windowId: win.id, tabId: win.tabs[0].id }); + } else if(msg == "nav") { + await browser.tabs.update(arg.tabId, { url: arg.url }) + browser.test.sendMessage("navigated"); + } else if (msg == "close") { + await browser.windows.remove(arg); + browser.test.sendMessage("closed"); + } + }); + }, + incognitoOverride: "spanning", + }); + await pb_extension.startup(); + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + let handlerUrl = await extension.awaitMessage("test-url"); + + // Ensure that the protocol handler is configured, and set it as default to + // bypass the dialog. + let chromeScript = SpecialPowers.loadChromeScript(protocolChromeScript); + + let msg = chromeScript.promiseOneMessage("handlerData"); + chromeScript.sendAsyncMessage("setup"); + let data = await msg; + ok(data.preferredAction, "using a helper application is the preferred action"); + ok(data.preferredApplicationHandler, "handler was set as default handler"); + is(data.handlers, 1, "one handler is set"); + ok(!data.alwaysAskBeforeHandling, "will not show dialog"); + ok(data.isWebHandler, "the handler is a web handler"); + is(data.uriTemplate, `${handlerUrl}?val=%s`, "correct url template"); + chromeScript.destroy(); + + extension.sendMessage("open", "ext+foo:test"); + let id = await extension.awaitMessage("opened"); + + let query = await extension.awaitMessage("test-query"); + is(query, "?val=ext%2Bfoo%3Atest", "test query ok"); + + extension.sendMessage("close", id); + await extension.awaitMessage("closed"); + + // Test the protocol in a private window, watch for the + // console error. + consoleMonitor.start([{message: /NS_ERROR_FILE_NOT_FOUND/}]); + + // Expect the chooser window to be open, close it. + chromeScript = SpecialPowers.loadChromeScript(async () => { + const CONTENT_HANDLING_URL = "chrome://mozapps/content/handling/appChooser.xhtml"; + const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + + let windowOpen = BrowserTestUtils.domWindowOpenedAndLoaded(); + + sendAsyncMessage("listenWindow"); + + let window = await windowOpen; + let gBrowser = window.gBrowser + let tabDialogBox = gBrowser.getTabDialogBox(gBrowser.selectedBrowser); + let dialogStack = tabDialogBox.getTabDialogManager()._dialogStack; + + let checkFn = dialogEvent => + dialogEvent.detail.dialog?._openedURL == CONTENT_HANDLING_URL; + + let eventPromise = BrowserTestUtils.waitForEvent( + dialogStack, + "dialogopen", + true, + checkFn + ); + + sendAsyncMessage("listenDialog"); + + let event = await eventPromise; + + let { dialog } = event.detail; + + let entry = dialog._frame.contentDocument.getElementById("items").firstChild; + sendAsyncMessage("handling", {name: entry.getAttribute("name"), disabled: entry.disabled}); + + dialog.close(); + }); + + // Wait for the chrome script to attach window listener + await chromeScript.promiseOneMessage("listenWindow"); + + let listenDialog = chromeScript.promiseOneMessage("listenDialog"); + let windowOpen = pb_extension.awaitMessage("opened"); + + pb_extension.sendMessage("open", "ext+foo:test"); + + // Wait for chrome script to attach dialog listener + await listenDialog; + let {tabId, windowId} = await windowOpen; + + let testData = chromeScript.promiseOneMessage("handling"); + let navPromise = pb_extension.awaitMessage("navigated"); + pb_extension.sendMessage("nav", {url: "ext+foo:test", tabId}); + await navPromise; + await consoleMonitor.finished(); + let entry = await testData; + + is(entry.name, "a foo protocol handler", "entry is correct"); + ok(entry.disabled, "handler is disabled"); + + let promiseClosed = pb_extension.awaitMessage("closed"); + pb_extension.sendMessage("close", windowId); + await promiseClosed; + await pb_extension.unload(); + + // Shutdown the addon, then ensure the protocol was removed. + await extension.unload(); + chromeScript = SpecialPowers.loadChromeScript(() => { + addMessageListener("setup", () => { + const protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService); + let protoInfo = protoSvc.getProtocolHandlerInfo("ext+foo"); + sendAsyncMessage("preferredApplicationHandler", !protoInfo.preferredApplicationHandler); + let handlers = protoInfo.possibleApplicationHandlers; + + sendAsyncMessage("handlerData", { + preferredApplicationHandler: !protoInfo.preferredApplicationHandler, + handlers: handlers.length, + }); + }); + }); + + msg = chromeScript.promiseOneMessage("handlerData"); + chromeScript.sendAsyncMessage("setup"); + data = await msg; + ok(data.preferredApplicationHandler, "no preferred handler is set"); + is(data.handlers, 0, "no handler is set"); + chromeScript.destroy(); +}); + +add_task(async function test_protocolHandler_two() { + let extensionData = { + manifest: { + "protocol_handlers": [ + { + "protocol": "ext+foo", + "name": "a foo protocol handler", + "uriTemplate": "foo.html?val=%s", + }, + { + "protocol": "ext+foo", + "name": "another foo protocol handler", + "uriTemplate": "foo2.html?val=%s", + }, + ], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + // Ensure that the protocol handler is configured, and set it as default, + // but because there are two handlers, the dialog is not bypassed. We + // don't test the actual dialog ui, it's been here forever and works based + // on the alwaysAskBeforeHandling value. + let chromeScript = SpecialPowers.loadChromeScript(protocolChromeScript); + + let msg = chromeScript.promiseOneMessage("handlerData"); + chromeScript.sendAsyncMessage("setup"); + let data = await msg; + ok(data.preferredAction, "using a helper application is the preferred action"); + ok(data.preferredApplicationHandler, "preferred handler is set"); + is(data.handlers, 2, "two handlers are set"); + ok(data.alwaysAskBeforeHandling, "will show dialog"); + ok(data.isWebHandler, "the handler is a web handler"); + chromeScript.destroy(); + await extension.unload(); +}); + +add_task(async function test_protocolHandler_https_target() { + let extensionData = { + manifest: { + "protocol_handlers": [ + { + "protocol": "ext+foo", + "name": "http target", + "uriTemplate": "https://example.com/foo.html?val=%s", + }, + ], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + ok(true, "https uriTemplate target works"); + await extension.unload(); +}); + +add_task(async function test_protocolHandler_http_target() { + let extensionData = { + manifest: { + "protocol_handlers": [ + { + "protocol": "ext+foo", + "name": "http target", + "uriTemplate": "http://example.com/foo.html?val=%s", + }, + ], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + ok(true, "http uriTemplate target works"); + await extension.unload(); +}); + +add_task(async function test_protocolHandler_restricted_protocol() { + let extensionData = { + manifest: { + "protocol_handlers": [ + { + "protocol": "http", + "name": "take over the http protocol", + "uriTemplate": "http.html?val=%s", + }, + ], + }, + }; + + consoleMonitor.start([{message: /processing protocol_handlers\.0\.protocol/}]); + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await Assert.rejects(extension.startup(), + /startup failed/, + "unable to register restricted handler protocol"); + + await consoleMonitor.finished(); +}); + +add_task(async function test_protocolHandler_restricted_uriTemplate() { + let extensionData = { + manifest: { + "protocol_handlers": [ + { + "protocol": "ext+foo", + "name": "take over the http protocol", + "uriTemplate": "ftp://example.com/file.txt", + }, + ], + }, + }; + + consoleMonitor.start([{message: /processing protocol_handlers\.0\.uriTemplate/}]); + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await Assert.rejects(extension.startup(), + /startup failed/, + "unable to register restricted handler uriTemplate"); + + await consoleMonitor.finished(); +}); + +add_task(async function test_protocolHandler_duplicate() { + let extensionData = { + manifest: { + "protocol_handlers": [ + { + "protocol": "ext+foo", + "name": "foo protocol", + "uriTemplate": "foo.html?val=%s", + }, + { + "protocol": "ext+foo", + "name": "foo protocol", + "uriTemplate": "foo.html?val=%s", + }, + ], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + // Get the count of handlers installed. + let chromeScript = SpecialPowers.loadChromeScript(() => { + addMessageListener("setup", () => { + const protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService); + let protoInfo = protoSvc.getProtocolHandlerInfo("ext+foo"); + let handlers = protoInfo.possibleApplicationHandlers; + sendAsyncMessage("handlerData", handlers.length); + }); + }); + + let msg = chromeScript.promiseOneMessage("handlerData"); + chromeScript.sendAsyncMessage("setup"); + let data = await msg; + is(data, 1, "cannot re-register the same handler config"); + chromeScript.destroy(); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_redirect_jar.html b/toolkit/components/extensions/test/mochitest/test_ext_redirect_jar.html new file mode 100644 index 0000000000..24dc737982 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_redirect_jar.html @@ -0,0 +1,92 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script> +"use strict"; + +function getExtension() { + return ExtensionTestUtils.loadExtension({ + manifest: { + "applications": { + "gecko": { + "id": "redirect-to-jar@mochi.test", + }, + }, + "permissions": [ + "webRequest", + "webRequestBlocking", + "<all_urls>", + ], + "web_accessible_resources": [ + "finished.html", + ], + }, + useAddonManager: "temporary", + files: { + "finished.html": ` + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h1>redirected!</h1> + </body> + </html> + `, + }, + background: async () => { + let redirectUrl = browser.extension.getURL("finished.html"); + browser.webRequest.onBeforeRequest.addListener(details => { + return {redirectUrl}; + }, {urls: ["*://*/intercept*"]}, ["blocking"]); + + let code = `new Promise(resolve => { + var s = document.createElement('iframe'); + s.src = "/intercept?r=" + Math.random(); + s.onload = async () => { + let url = await window.wrappedJSObject.SpecialPowers.spawn(s, [], () => content.location.href ); + resolve(['loaded', url]); + } + s.onerror = () => resolve(['error']); + document.documentElement.appendChild(s); + });`; + + async function testSubFrameResource(tabId, code) { + let [result] = await browser.tabs.executeScript(tabId, { code }); + return result; + } + + let tab = await browser.tabs.create({url: "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_sample.html"}); + let result = await testSubFrameResource(tab.id, code); + browser.test.assertEq("loaded", result[0], "frame 1 loaded"); + browser.test.assertEq(redirectUrl, result[1], "frame 1 redirected"); + // If jar caching breaks redirects, this next test will fail (See Bug 1390346). + result = await testSubFrameResource(tab.id, code); + browser.test.assertEq("loaded", result[0], "frame 2 loaded"); + browser.test.assertEq(redirectUrl, result[1], "frame 2 redirected"); + await browser.tabs.remove(tab.id); + browser.test.sendMessage("requestsCompleted"); + }, + }); +} + +add_task(async function test_redirect_to_jar() { + let extension = getExtension(); + await extension.startup(); + await extension.awaitMessage("requestsCompleted"); + await extension.unload(); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_request_urlClassification.html b/toolkit/components/extensions/test/mochitest/test_ext_request_urlClassification.html new file mode 100644 index 0000000000..d9a85ad8e4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_request_urlClassification.html @@ -0,0 +1,129 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebRequest urlClassification</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.trackingprotection.enabled", true]], + }); + + let chromeScript = SpecialPowers.loadChromeScript(async _ => { + const {UrlClassifierTestUtils} = ChromeUtils.import("resource://testing-common/UrlClassifierTestUtils.jsm"); + await UrlClassifierTestUtils.addTestTrackers(); + sendAsyncMessage("trackersLoaded"); + }); + await chromeScript.promiseOneMessage("trackersLoaded"); + chromeScript.destroy(); +}); + +add_task(async function test_urlClassification() { + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + applications: {gecko: {id: "classification@mochi.test"}}, + permissions: ["webRequest", "webRequestBlocking", "proxy", "<all_urls>"], + }, + background() { + let expected = { + "http://tracking.example.org/": {first: "tracking", thirdParty: false, }, + "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_third_party.html?domain=tracking.example.org": { thirdParty: false, }, + "http://tracking.example.org/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png": {third: "tracking", thirdParty: true, }, + "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_third_party.html?domain=example.net": { thirdParty: false, }, + "http://example.net/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png": { thirdParty: true, }, + }; + function testRequest(details) { + let expect = expected[details.url]; + if (expect) { + if (expect.first) { + browser.test.assertTrue(details.urlClassification.firstParty.includes("tracking"), "tracking firstParty"); + } else { + browser.test.assertEq(details.urlClassification.firstParty.length, 0, "not tracking firstParty"); + } + if (expect.third) { + browser.test.assertTrue(details.urlClassification.thirdParty.includes("tracking"), "tracking thirdParty"); + } else { + browser.test.assertEq(details.urlClassification.thirdParty.length, 0, "not tracking thirdParty"); + } + + browser.test.assertEq(details.thirdParty, expect.thirdParty, "3rd party flag matches"); + return true; + } + return false; + } + + browser.proxy.onRequest.addListener(details => { + browser.test.log(`proxy.onRequest ${JSON.stringify(details)}`); + testRequest(details); + }, {urls: ["http://mochi.test/tests/*", "http://tracking.example.org/*", "http://example.net/*"]}); + browser.webRequest.onBeforeRequest.addListener(async (details) => { + browser.test.log(`webRequest.onBeforeRequest ${JSON.stringify(details)}`); + testRequest(details); + }, {urls: ["http://mochi.test/tests/*", "http://tracking.example.org/*", "http://example.net/*"]}, ["blocking"]); + browser.webRequest.onCompleted.addListener(async (details) => { + browser.test.log(`webRequest.onCompleted ${JSON.stringify(details)}`); + if (testRequest(details)) { + browser.test.sendMessage("classification", details.url); + } + }, {urls: ["http://mochi.test/tests/*", "http://tracking.example.org/*", "http://example.net/*"]}); + }, + }); + await extension.startup(); + + // Test first party tracking classification. + let url = "http://tracking.example.org/"; + let win = window.open(url); + is(await extension.awaitMessage("classification"), url, "request completed"); + win.close(); + + // Test third party tracking classification, expecting two results. + url = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_third_party.html?domain=tracking.example.org"; + win = window.open(url); + is(await extension.awaitMessage("classification"), url); + is(await extension.awaitMessage("classification"), + "http://tracking.example.org/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png", + "request completed"); + win.close(); + + // Test third party tracking classification, expecting two results. + url = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_third_party.html?domain=example.net"; + win = window.open(url); + is(await extension.awaitMessage("classification"), url); + is(await extension.awaitMessage("classification"), + "http://example.net/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png", + "request completed"); + win.close(); + + await extension.unload(); +}); + +add_task(async function teardown() { + let chromeScript = SpecialPowers.loadChromeScript(async _ => { + // Cleanup cache + await new Promise(resolve => { + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); + }); + + /* global sendAsyncMessage */ + const {UrlClassifierTestUtils} = ChromeUtils.import("resource://testing-common/UrlClassifierTestUtils.jsm"); + await UrlClassifierTestUtils.cleanupTestTrackers(); + sendAsyncMessage("trackersUnloaded"); + }); + await chromeScript.promiseOneMessage("trackersUnloaded"); + chromeScript.destroy(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html new file mode 100644 index 0000000000..c4726092ec --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html @@ -0,0 +1,82 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function background() { + browser.runtime.onConnect.addListener(port => { + browser.test.assertEq(port.name, "ernie", "port name correct"); + browser.test.assertTrue(port.sender.url.endsWith("file_sample.html"), "URL correct"); + browser.test.assertTrue(port.sender.tab.url.endsWith("file_sample.html"), "tab URL correct"); + + let expected = "message 1"; + port.onMessage.addListener(msg => { + browser.test.assertEq(msg, expected, "message is expected"); + if (expected == "message 1") { + port.postMessage("message 2"); + expected = "message 3"; + } else if (expected == "message 3") { + expected = "disconnect"; + browser.test.notifyPass("runtime.connect"); + } + }); + port.onDisconnect.addListener(() => { + browser.test.assertEq(null, port.error, "No error because port is closed by disconnect() at other end"); + browser.test.assertEq(expected, "disconnect", "got disconnection at right time"); + }); + }); +} + +function contentScript() { + let port = browser.runtime.connect({name: "ernie"}); + port.postMessage("message 1"); + port.onMessage.addListener(msg => { + if (msg == "message 2") { + port.postMessage("message 3"); + port.disconnect(); + } + }); +} + +let extensionData = { + background, + manifest: { + "permissions": ["tabs"], + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_start", + }], + }, + + files: { + "content_script.js": contentScript, + }, +}; + +add_task(async function test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let win = window.open("file_sample.html"); + + await Promise.all([waitForLoad(win), extension.awaitFinish("runtime.connect")]); + + win.close(); + + await extension.unload(); + info("extension unloaded"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html new file mode 100644 index 0000000000..13b9029c48 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html @@ -0,0 +1,102 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function backgroundScript(token) { + browser.runtime.onMessage.addListener(msg => { + browser.test.assertEq(msg, "done"); + browser.test.notifyPass("sendmessage_reply"); + }); + + browser.runtime.onConnect.addListener(port => { + browser.test.assertTrue(port.sender.url.endsWith("file_sample.html"), "sender url correct"); + browser.test.assertTrue(port.sender.tab.url.endsWith("file_sample.html"), "sender url correct"); + + let tabId = port.sender.tab.id; + browser.tabs.connect(tabId, {name: token}); + + browser.test.assertEq(port.name, token, "token matches"); + port.postMessage(token + "-done"); + }); + + browser.test.sendMessage("background-ready"); +} + +function contentScript(token) { + let gotTabMessage = false; + let badTabMessage = false; + browser.runtime.onConnect.addListener(port => { + if (port.name == token) { + gotTabMessage = true; + } else { + badTabMessage = true; + } + port.disconnect(); + }); + + let port = browser.runtime.connect(null, {name: token}); + port.onMessage.addListener(function(msg) { + if (msg != token + "-done" || !gotTabMessage || badTabMessage) { + return; // test failed + } + + // FIXME: Removing this line causes the test to fail: + // resource://gre/modules/ExtensionUtils.jsm, line 651: NS_ERROR_NOT_INITIALIZED + port.disconnect(); + browser.runtime.sendMessage("done"); + }); +} + +function makeExtension() { + let token = Math.random(); + let extensionData = { + background: `(${backgroundScript})("${token}")`, + manifest: { + "permissions": ["tabs"], + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_idle", + }], + }, + + files: { + "content_script.js": `(${contentScript})("${token}")`, + }, + }; + return extensionData; +} + +add_task(async function test_contentscript() { + let extension1 = ExtensionTestUtils.loadExtension(makeExtension()); + let extension2 = ExtensionTestUtils.loadExtension(makeExtension()); + await Promise.all([extension1.startup(), extension2.startup()]); + + await extension1.awaitMessage("background-ready"); + await extension2.awaitMessage("background-ready"); + + let win = window.open("file_sample.html"); + + await Promise.all([waitForLoad(win), + extension1.awaitFinish("sendmessage_reply"), + extension2.awaitFinish("sendmessage_reply")]); + + win.close(); + + await extension1.unload(); + await extension2.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html new file mode 100644 index 0000000000..b671cba23d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html @@ -0,0 +1,126 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> + +<script> +"use strict"; + +add_task(async function test_connect_bidirectionally_and_postMessage() { + function background() { + let onConnectCount = 0; + browser.runtime.onConnect.addListener(port => { + // 3. onConnect by connect() from CS. + browser.test.assertEq("from-cs", port.name); + browser.test.assertEq(1, ++onConnectCount, + "BG onConnect should be called once"); + + let tabId = port.sender.tab.id; + browser.test.assertTrue(tabId, "content script must have a tab ID"); + + let port2; + let postMessageCount1 = 0; + port.onMessage.addListener(msg => { + // 11. port.onMessage by port.postMessage in CS. + browser.test.assertEq("from CS to port", msg); + browser.test.assertEq(1, ++postMessageCount1, + "BG port.onMessage should be called once"); + + // 12. should trigger port2.onMessage in CS. + port2.postMessage("from BG to port2"); + }); + + // 4. Should trigger onConnect in CS. + port2 = browser.tabs.connect(tabId, {name: "from-bg"}); + let postMessageCount2 = 0; + port2.onMessage.addListener(msg => { + // 7. onMessage by port2.postMessage in CS. + browser.test.assertEq("from CS to port2", msg); + browser.test.assertEq(1, ++postMessageCount2, + "BG port2.onMessage should be called once"); + + // 8. Should trigger port.onMessage in CS. + port.postMessage("from BG to port"); + }); + }); + + // 1. Notify test runner to create a new tab. + browser.test.sendMessage("ready"); + } + + function contentScript() { + let onConnectCount = 0; + let port; + browser.runtime.onConnect.addListener(port2 => { + // 5. onConnect by connect() from BG. + browser.test.assertEq("from-bg", port2.name); + browser.test.assertEq(1, ++onConnectCount, + "CS onConnect should be called once"); + + let postMessageCount2 = 0; + port2.onMessage.addListener(msg => { + // 12. port2.onMessage by port2.postMessage in BG. + browser.test.assertEq("from BG to port2", msg); + browser.test.assertEq(1, ++postMessageCount2, + "CS port2.onMessage should be called once"); + + // TODO(robwu): Do not explicitly disconnect, it should not be a problem + // if we keep the ports open. However, not closing the ports causes the + // test to fail with NS_ERROR_NOT_INITIALIZED in ExtensionUtils.jsm, in + // Port.prototype.disconnect (nsIMessageSender.sendAsyncMessage). + port.disconnect(); + port2.disconnect(); + browser.test.notifyPass("ping pong done"); + }); + // 6. should trigger port2.onMessage in BG. + port2.postMessage("from CS to port2"); + }); + + // 2. should trigger onConnect in BG. + port = browser.runtime.connect({name: "from-cs"}); + let postMessageCount1 = 0; + port.onMessage.addListener(msg => { + // 9. onMessage by port.postMessage in BG. + browser.test.assertEq("from BG to port", msg); + browser.test.assertEq(1, ++postMessageCount1, + "CS port.onMessage should be called once"); + + // 10. should trigger port.onMessage in BG. + port.postMessage("from CS to port"); + }); + } + + let extensionData = { + background, + manifest: { + content_scripts: [{ + js: ["contentscript.js"], + matches: ["http://mochi.test/*/file_sample.html"], + }], + }, + files: { + "contentscript.js": contentScript, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + info("extension loaded"); + + await extension.awaitMessage("ready"); + + let win = window.open("file_sample.html"); + await extension.awaitFinish("ping pong done"); + win.close(); + + await extension.unload(); + info("extension unloaded"); +}); +</script> +</body> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html new file mode 100644 index 0000000000..f18190bf8b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function background() { + browser.runtime.onConnect.addListener(port => { + browser.test.assertEq(port.name, "ernie", "port name correct"); + port.onDisconnect.addListener(() => { + browser.test.assertEq(null, port.error, "The port is implicitly closed without errors when the other context unloads"); + // Closing an already-disconnected port is a no-op. + port.disconnect(); + port.disconnect(); + browser.test.sendMessage("disconnected"); + }); + browser.test.sendMessage("connected"); + }); +} + +function contentScript() { + browser.runtime.connect({name: "ernie"}); +} + +let extensionData = { + background, + manifest: { + "permissions": ["tabs"], + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_idle", + }], + }, + + files: { + "content_script.js": contentScript, + }, +}; + +add_task(async function test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let win = window.open("file_sample.html"); + await Promise.all([waitForLoad(win), extension.awaitMessage("connected")]); + win.close(); + await extension.awaitMessage("disconnected"); + + info("win.close() succeeded"); + + win = window.open("file_sample.html"); + await Promise.all([waitForLoad(win), extension.awaitMessage("connected")]); + + // Add an "unload" listener so that we don't put the window in the + // bfcache. This way it gets destroyed immediately upon navigation. + win.addEventListener("unload", function() {}); // eslint-disable-line mozilla/balanced-listeners + + win.location = "http://example.com"; + await extension.awaitMessage("disconnected"); + win.close(); + + await extension.unload(); + info("extension unloaded"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html new file mode 100644 index 0000000000..ffdbc90efb --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function background() { + // Add two listeners that both send replies. We're supposed to ignore all but one + // of them. Which one is chosen is non-deterministic. + + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct"); + + if (msg == "getreply") { + sendReply("reply1"); + } + }); + + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct"); + + if (msg == "getreply") { + sendReply("reply2"); + } + }); + + function sleep(callback, n = 10) { + if (n == 0) { + callback(); + } else { + setTimeout(function() { sleep(callback, n - 1); }, 0); + } + } + + let done_count = 0; + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct"); + + if (msg == "done") { + done_count++; + browser.test.assertEq(done_count, 1, "got exactly one reply"); + + // Go through the event loop a few times to make sure we don't get multiple replies. + sleep(function() { + browser.test.notifyPass("sendmessage_doublereply"); + }); + } + }); +} + +function contentScript() { + browser.runtime.sendMessage("getreply", function(resp) { + if (resp != "reply1" && resp != "reply2") { + return; // test failed + } + browser.runtime.sendMessage("done"); + }); +} + +let extensionData = { + background, + manifest: { + "permissions": ["tabs"], + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_start", + }], + }, + + files: { + "content_script.js": contentScript, + }, +}; + +add_task(async function test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let win = window.open("file_sample.html"); + + await Promise.all([waitForLoad(win), extension.awaitFinish("sendmessage_doublereply")]); + + win.close(); + + await extension.unload(); + info("extension unloaded"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_frameId.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_frameId.html new file mode 100644 index 0000000000..ca151b0216 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_frameId.html @@ -0,0 +1,49 @@ +<!doctype html> +<head> + <title>Test sendMessage frameId</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<script> +"use strict"; + +add_task(async function test_sendMessage_frameId() { + const html = `<!doctype html><meta charset="utf-8"><script src="script.js"><\/script>`; + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + applications: { gecko: { id: "send_message_frame_id@tests.mozilla.org" } }, + }, + background() { + browser.runtime.onMessage.addListener((msg, sender) => { + browser.test.sendMessage(msg, sender); + }); + browser.tabs.create({url: "tab.html"}); + }, + files: { + "iframe.html": html, + "tab.html": `${html}<iframe src="iframe.html"></iframe>`, + "script.js": () => { + browser.runtime.sendMessage(window.top === window ? "tab" : "iframe"); + }, + }, + }); + + await extension.startup(); + + const tab = await extension.awaitMessage("tab"); + ok(tab.url.endsWith("tab.html"), "Got the message from the tab"); + is(tab.frameId, 0, "And sender.frameId is zero"); + + const iframe = await extension.awaitMessage("iframe"); + ok(iframe.url.endsWith("iframe.html"), "Got the message from the iframe"); + is(typeof iframe.frameId, "number", "With sender.frameId of type number"); + ok(iframe.frameId > 0, "And sender.frameId greater than zero"); + + await extension.unload(); +}); + +</script> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html new file mode 100644 index 0000000000..970de26528 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<html> +<head> + <title>WebExtension test</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> +<script> +"use strict"; + +function loadContentScriptExtension(contentScript) { + let extensionData = { + manifest: { + "content_scripts": [{ + "js": ["contentscript.js"], + "matches": ["http://mochi.test/*/file_sample.html"], + }], + }, + files: { + "contentscript.js": contentScript, + }, + }; + return ExtensionTestUtils.loadExtension(extensionData); +} + +add_task(async function test_content_script_sendMessage_without_listener() { + async function contentScript() { + await browser.test.assertRejects( + browser.runtime.sendMessage("msg"), + "Could not establish connection. Receiving end does not exist."); + + browser.test.notifyPass("sendMessage callback was invoked"); + } + + let extension = loadContentScriptExtension(contentScript); + await extension.startup(); + + let win = window.open("file_sample.html"); + await extension.awaitFinish("sendMessage callback was invoked"); + win.close(); + + await extension.unload(); +}); + +add_task(async function test_content_script_chrome_sendMessage_without_listener() { + function contentScript() { + /* globals chrome */ + browser.test.assertEq(null, chrome.runtime.lastError, "no lastError before call"); + let retval = chrome.runtime.sendMessage("msg"); + browser.test.assertEq(null, chrome.runtime.lastError, "no lastError after call"); + // TODO(robwu): Fix the implementation and uncomment the next expectation. + // When content script APIs are schema-based (bugzil.la/1287007) this bug will be fixed for free. + // browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage without callback"); + browser.test.assertTrue(retval instanceof Promise, "TODO: chrome.runtime.sendMessage should return undefined, not a promise"); + + let isAsyncCall = false; + retval = chrome.runtime.sendMessage("msg", reply => { + browser.test.assertEq(undefined, reply, "no reply"); + browser.test.assertTrue(isAsyncCall, "chrome.runtime.sendMessage's callback must be called asynchronously"); + browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage with callback"); + browser.test.assertEq("Could not establish connection. Receiving end does not exist.", chrome.runtime.lastError.message); + browser.test.notifyPass("finished chrome.runtime.sendMessage"); + }); + isAsyncCall = true; + } + + let extension = loadContentScriptExtension(contentScript); + await extension.startup(); + + let win = window.open("file_sample.html"); + await extension.awaitFinish("finished chrome.runtime.sendMessage"); + win.close(); + + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html new file mode 100644 index 0000000000..a7f6314efd --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html @@ -0,0 +1,78 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function background() { + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct"); + + if (msg == 0) { + sendReply("reply1"); + } else if (msg == 1) { + window.setTimeout(function() { + sendReply("reply2"); + }, 0); + return true; + } else if (msg == 2) { + browser.test.notifyPass("sendmessage_reply"); + } + }); +} + +function contentScript() { + browser.runtime.sendMessage(0, function(resp1) { + if (resp1 != "reply1") { + return; // test failed + } + browser.runtime.sendMessage(1, function(resp2) { + if (resp2 != "reply2") { + return; // test failed + } + browser.runtime.sendMessage(2); + }); + }); +} + +let extensionData = { + background, + manifest: { + "permissions": ["tabs"], + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_idle", + }], + }, + + files: { + "content_script.js": contentScript, + }, +}; + +add_task(async function test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let win = window.open("file_sample.html"); + + await Promise.all([waitForLoad(win), extension.awaitFinish("sendmessage_reply")]); + + win.close(); + + await extension.unload(); + info("extension unloaded"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html new file mode 100644 index 0000000000..d3227dbcaf --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html @@ -0,0 +1,204 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function backgroundScript(token, id, otherId) { + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`); + + if (msg === `content-${token}`) { + browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), + `${id}: sender url correct`); + + let tabId = sender.tab.id; + browser.tabs.sendMessage(tabId, `${token}-contentMessage`); + + sendReply(`${token}-done`); + } else if (msg === `tab-${token}`) { + browser.runtime.sendMessage(otherId, `${otherId}-tabMessage`); + browser.runtime.sendMessage(`${token}-tabMessage`); + + sendReply(`${token}-done`); + } else { + browser.test.fail(`${id}: Unexpected runtime message received: ${msg} ${uneval(sender)}`); + } + }); + + browser.runtime.onMessageExternal.addListener((msg, sender, sendReply) => { + browser.test.assertEq(otherId, sender.id, `${id}: Got expected external sender ID`); + + if (msg === `content-${id}`) { + browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), + `${id}: external sender url correct`); + + sendReply(`${otherId}-done`); + } else if (msg === `tab-${id}`) { + sendReply(`${otherId}-done`); + } else if (msg !== `${id}-tabMessage`) { + browser.test.fail(`${id}: Unexpected runtime external message received: ${msg} ${uneval(sender)}`); + } + }); + + browser.tabs.create({url: "tab.html"}); +} + +function contentScript(token, id, otherId) { + let gotContentMessage = false; + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`); + + browser.test.assertEq(`${token}-contentMessage`, msg, + `${id}: Correct content script message`); + if (msg === `${token}-contentMessage`) { + gotContentMessage = true; + } + }); + + Promise.all([ + browser.runtime.sendMessage(otherId, `content-${otherId}`).then(resp => { + browser.test.assertEq(`${id}-done`, resp, `${id}: Correct content script external response token`); + }), + + browser.runtime.sendMessage(`content-${token}`).then(resp => { + browser.test.assertEq(`${token}-done`, resp, `${id}: Correct content script response token`); + }).catch(e => { + browser.test.fail(`content-${token} rejected with ${e.message}`); + }), + ]).then(() => { + browser.test.assertTrue(gotContentMessage, `${id}: Got content script message`); + + browser.test.sendMessage("content-script-done"); + }); +} + +async function tabScript(token, id, otherId) { + let gotTabMessage = false; + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`); + + if (String(msg).startsWith("content-")) { + return; + } + + browser.test.assertEq(`${token}-tabMessage`, msg, + `${id}: Correct tab script message`); + if (msg === `${token}-tabMessage`) { + gotTabMessage = true; + } + }); + + browser.test.sendMessage("tab-script-loaded"); + + await new Promise(resolve => { + const listener = (msg) => { + if (msg !== "run-tab-script") { + return; + } + browser.test.onMessage.removeListener(listener); + resolve(); + }; + browser.test.onMessage.addListener(listener); + }); + + Promise.all([ + browser.runtime.sendMessage(otherId, `tab-${otherId}`).then(resp => { + browser.test.assertEq(`${id}-done`, resp, `${id}: Correct tab script external response token`); + }), + + browser.runtime.sendMessage(`tab-${token}`).then(resp => { + browser.test.assertEq(`${token}-done`, resp, `${id}: Correct tab script response token`); + }), + ]).then(() => { + browser.test.assertTrue(gotTabMessage, `${id}: Got tab script message`); + + window.close(); + + browser.test.sendMessage("tab-script-done"); + }); +} + +function makeExtension(id, otherId) { + let token = Math.random(); + + let args = `${token}, ${JSON.stringify(id)}, ${JSON.stringify(otherId)}`; + + let extensionData = { + useAddonManager: "permanent", + background: `(${backgroundScript})(${args})`, + manifest: { + "applications": {"gecko": {id}}, + + "permissions": ["tabs"], + + + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_start", + }], + }, + + files: { + "tab.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <script src="tab.js"><\/script> + </head> + </html>`, + + "tab.js": `(${tabScript})(${args})`, + + "content_script.js": `(${contentScript})(${args})`, + }, + }; + return extensionData; +} + +add_task(async function test_contentscript() { + const ID1 = "sendmessage1@mochitest.mozilla.org"; + const ID2 = "sendmessage2@mochitest.mozilla.org"; + + let extension1 = ExtensionTestUtils.loadExtension(makeExtension(ID1, ID2)); + let extension2 = ExtensionTestUtils.loadExtension(makeExtension(ID2, ID1)); + + await Promise.all([ + extension1.startup(), + extension2.startup(), + extension1.awaitMessage("tab-script-loaded"), + extension2.awaitMessage("tab-script-loaded"), + ]); + + extension1.sendMessage("run-tab-script"); + extension2.sendMessage("run-tab-script"); + + let win = window.open("file_sample.html"); + + await waitForLoad(win); + + await Promise.all([ + extension1.awaitMessage("content-script-done"), + extension2.awaitMessage("content-script-done"), + extension1.awaitMessage("tab-script-done"), + extension2.awaitMessage("tab-script-done"), + ]); + + win.close(); + + await extension1.unload(); + await extension2.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_storage_cleanup.html b/toolkit/components/extensions/test/mochitest/test_ext_storage_cleanup.html new file mode 100644 index 0000000000..e02b016419 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_cleanup.html @@ -0,0 +1,235 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const { + ExtensionStorageIDB, +} = SpecialPowers.Cu.import("resource://gre/modules/ExtensionStorageIDB.jsm"); + +const storageTestHelpers = { + storageLocal: { + async writeData() { + await browser.storage.local.set({hello: "world"}); + browser.test.sendMessage("finished"); + }, + + async readData() { + const matchBrowserStorage = await browser.storage.local.get("hello").then(result => { + return (Object.keys(result).length == 1 && result.hello == "world"); + }); + + browser.test.sendMessage("results", {matchBrowserStorage}); + }, + + assertResults({results, keepOnUninstall}) { + if (keepOnUninstall) { + is(results.matchBrowserStorage, true, "browser.storage.local data is still present"); + } else { + is(results.matchBrowserStorage, false, "browser.storage.local data was cleared"); + } + }, + }, + webAPIs: { + async readData() { + let matchLocalStorage = (localStorage.getItem("hello") == "world"); + + let idbPromise = new Promise((resolve, reject) => { + let req = indexedDB.open("test"); + req.onerror = e => { + reject(new Error(`indexedDB open failed with ${e.errorCode}`)); + }; + + req.onupgradeneeded = e => { + // no database, data is not present + resolve(false); + }; + + req.onsuccess = e => { + let db = e.target.result; + let transaction = db.transaction("store", "readwrite"); + let addreq = transaction.objectStore("store").get("hello"); + addreq.onerror = addreqError => { + reject(new Error(`read from indexedDB failed with ${addreqError.errorCode}`)); + }; + addreq.onsuccess = () => { + let match = (addreq.result.value == "world"); + resolve(match); + }; + }; + }); + + await idbPromise.then(matchIDB => { + let result = {matchLocalStorage, matchIDB}; + browser.test.sendMessage("results", result); + }); + }, + + async writeData() { + localStorage.setItem("hello", "world"); + + let idbPromise = new Promise((resolve, reject) => { + let req = indexedDB.open("test"); + req.onerror = e => { + reject(new Error(`indexedDB open failed with ${e.errorCode}`)); + }; + + req.onupgradeneeded = e => { + let db = e.target.result; + db.createObjectStore("store", {keyPath: "name"}); + }; + + req.onsuccess = e => { + let db = e.target.result; + let transaction = db.transaction("store", "readwrite"); + let addreq = transaction.objectStore("store") + .add({name: "hello", value: "world"}); + addreq.onerror = addreqError => { + reject(new Error(`add to indexedDB failed with ${addreqError.errorCode}`)); + }; + addreq.onsuccess = () => { + resolve(); + }; + }; + }); + + await idbPromise.then(() => { + browser.test.sendMessage("finished"); + }); + }, + + assertResults({results, keepOnUninstall}) { + if (keepOnUninstall) { + is(results.matchLocalStorage, true, "localStorage data is still present"); + is(results.matchIDB, true, "indexedDB data is still present"); + } else { + is(results.matchLocalStorage, false, "localStorage data was cleared"); + is(results.matchIDB, false, "indexedDB data was cleared"); + } + }, + }, +}; + +async function test_uninstall({extensionId, writeData, readData, assertResults}) { + // Set the pref to prevent cleaning up storage on uninstall in a separate prefEnv + // so we can pop it below, leaving flags set in the previous prefEnvs unmodified. + await SpecialPowers.pushPrefEnv({ + set: [["extensions.webextensions.keepStorageOnUninstall", true]], + }); + + let extension = ExtensionTestUtils.loadExtension({ + background: writeData, + manifest: { + applications: {gecko: {id: extensionId}}, + permissions: ["storage"], + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + await extension.awaitMessage("finished"); + await extension.unload(); + + // Check that we can still see data we wrote to storage but clear the + // "leave storage" flag so our storaged gets cleared on the next uninstall. + // This effectively tests the keepUuidOnUninstall logic, which ensures + // that when we read storage again and check that it is cleared, that + // it is actually a meaningful test! + await SpecialPowers.popPrefEnv(); + + extension = ExtensionTestUtils.loadExtension({ + background: readData, + manifest: { + applications: {gecko: {id: extensionId}}, + permissions: ["storage"], + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + let results = await extension.awaitMessage("results"); + + assertResults({results, keepOnUninstall: true}); + + await extension.unload(); + + // Read again. This time, our data should be gone. + extension = ExtensionTestUtils.loadExtension({ + background: readData, + manifest: { + applications: {gecko: {id: extensionId}}, + permissions: ["storage"], + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + results = await extension.awaitMessage("results"); + + assertResults({results, keepOnUninstall: false}); + + await extension.unload(); +} + + +add_task(async function test_setup_keep_uuid_on_uninstall() { + // Use a test-only pref to leave the addonid->uuid mapping around after + // uninstall so that we can re-attach to the same storage (this prefEnv + // is kept for this entire file and cleared automatically once all the + // tests in this file have been executed). + await SpecialPowers.pushPrefEnv({ + set: [["extensions.webextensions.keepUuidOnUninstall", true]], + }); +}); + +// Test extension indexedDB and localStorage storages get cleaned up when the +// extension is uninstalled. +add_task(async function test_uninstall_with_webapi_storages() { + await test_uninstall({ + extensionId: "storage.cleanup-WebAPIStorages@tests.mozilla.org", + ...(storageTestHelpers.webAPIs), + }); +}); + +// Test browser.storage.local with JSONFile backend gets cleaned up when the +// extension is uninstalled. +add_task(async function test_uninistall_with_storage_local_file_backend() { + await SpecialPowers.pushPrefEnv({ + set: [[ExtensionStorageIDB.BACKEND_ENABLED_PREF, false]], + }); + + await test_uninstall({ + extensionId: "storage.cleanup-JSONFileBackend@tests.mozilla.org", + ...(storageTestHelpers.storageLocal), + }); + + await SpecialPowers.popPrefEnv(); +}); + +// Repeat the cleanup test when the storage.local IndexedDB backend is enabled. +add_task(async function test_uninistall_with_storage_local_idb_backend() { + await SpecialPowers.pushPrefEnv({ + set: [[ExtensionStorageIDB.BACKEND_ENABLED_PREF, true]], + }); + + await test_uninstall({ + extensionId: "storage.cleanup-IDBBackend@tests.mozilla.org", + ...(storageTestHelpers.storageLocal), + }); + + await SpecialPowers.popPrefEnv(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_storage_manager_capabilities.html b/toolkit/components/extensions/test/mochitest/test_ext_storage_manager_capabilities.html new file mode 100644 index 0000000000..3a02f3fb63 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_manager_capabilities.html @@ -0,0 +1,126 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test Storage API </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + "set": [ + ["dom.storageManager.enabled", true], + ["dom.storageManager.prompt.testing", true], + ["dom.storageManager.prompt.testing.allow", true], + ], + }); +}); + +add_task(async function test_backgroundScript() { + function background() { + browser.test.assertTrue(navigator.storage !== undefined, "Has storage api interface"); + + // Test estimate. + browser.test.assertTrue("estimate" in navigator.storage, "Has estimate function"); + browser.test.assertEq("function", typeof navigator.storage.estimate, "estimate is function"); + browser.test.assertTrue(navigator.storage.estimate() instanceof Promise, "estimate returns a promise"); + + return browser.test.notifyPass("navigation_storage_api.done"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + }); + + await extension.startup(); + await extension.awaitFinish("navigation_storage_api.done"); + await extension.unload(); +}); + +add_task(async function test_contentScript() { + function contentScript() { + // Should not access storage api in non-secure context. + browser.test.assertEq(undefined, navigator.storage, + "A page from the unsecure http protocol " + + "doesn't have access to the navigator.storage API"); + + return browser.test.notifyPass("navigation_storage_api.done"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [{ + "matches": ["http://example.com/*/file_sample.html"], + "js": ["content_script.js"], + }], + }, + + files: { + "content_script.js": `(${contentScript})()`, + }, + }); + + await extension.startup(); + + // Open an explicit URL for testing Storage API in an insecure context. + let win = window.open("http://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"); + + await extension.awaitFinish("navigation_storage_api.done"); + + await extension.unload(); + win.close(); +}); + +add_task(async function test_contentScriptSecure() { + function contentScript() { + browser.test.assertTrue(navigator.storage !== undefined, "Has storage api interface"); + + // Test estimate. + browser.test.assertTrue("estimate" in navigator.storage, "Has estimate function"); + browser.test.assertEq("function", typeof navigator.storage.estimate, "estimate is function"); + + // The promise that estimate function returns belongs to the content page, + // but the Promise constructor belongs to the content script sandbox. + // Check window.Promise here. + browser.test.assertTrue(navigator.storage.estimate() instanceof window.Promise, "estimate returns a promise"); + + return browser.test.notifyPass("navigation_storage_api.done"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [{ + "matches": ["https://example.com/*/file_sample.html"], + "js": ["content_script.js"], + }], + }, + + files: { + "content_script.js": `(${contentScript})()`, + }, + }); + + await extension.startup(); + + // Open an explicit URL for testing Storage API in a secure context. + let win = window.open("file_sample.html"); + + await extension.awaitFinish("navigation_storage_api.done"); + + await extension.unload(); + win.close(); +}); + +add_task(async function cleanup() { + await SpecialPowers.popPrefEnv(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_storage_smoke_test.html b/toolkit/components/extensions/test/mochitest/test_ext_storage_smoke_test.html new file mode 100644 index 0000000000..b0c7425383 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_smoke_test.html @@ -0,0 +1,110 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +// The purpose of this test is making sure that the implementation enabled by +// default for the storage.local and storage.sync APIs does work across all +// platforms/builds/apps +add_task(async function test_storage_smoke_test() { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + for (let storageArea of ["sync", "local"]) { + let storage = browser.storage[storageArea]; + + browser.test.assertTrue(!!storage, `StorageArea ${storageArea} is present.`) + + let data = await storage.get(); + browser.test.assertEq(0, Object.keys(data).length, + `Storage starts out empty for ${storageArea}`); + + data = await storage.get("test"); + browser.test.assertEq(0, Object.keys(data).length, + `Can read non-existent keys for ${storageArea}`); + + await storage.set({ + "test1": "test-value1", + "test2": "test-value2", + "test3": "test-value3" + }); + + browser.test.assertEq( + "test-value1", + (await storage.get("test1")).test1, + `Can set and read back single values for ${storageArea}`); + + browser.test.assertEq( + "test-value2", + (await storage.get("test2")).test2, + `Can set and read back single values for ${storageArea}`); + + data = await storage.get(); + browser.test.assertEq(3, Object.keys(data).length, + `Can set and read back all values for ${storageArea}`); + browser.test.assertEq("test-value1", data.test1, + `Can set and read back all values for ${storageArea}`); + browser.test.assertEq("test-value2", data.test2, + `Can set and read back all values for ${storageArea}`); + browser.test.assertEq("test-value3", data.test3, + `Can set and read back all values for ${storageArea}`); + + data = await storage.get(["test1", "test2"]); + browser.test.assertEq(2, Object.keys(data).length, + `Can set and read back array of values for ${storageArea}`); + browser.test.assertEq("test-value1", data.test1, + `Can set and read back array of values for ${storageArea}`); + browser.test.assertEq("test-value2", data.test2, + `Can set and read back array of values for ${storageArea}`); + + await storage.remove("test1"); + data = await storage.get(["test1", "test2"]); + browser.test.assertEq(1, Object.keys(data).length, + `Data can be removed for ${storageArea}`); + browser.test.assertEq("test-value2", data.test2, + `Data can be removed for ${storageArea}`); + + data = await storage.get({ + test1: 1, + test2: 2, + }); + browser.test.assertEq(2, Object.keys(data).length, + `Expected a key-value pair for every property for ${storageArea}`); + browser.test.assertEq(1, data.test1, + `Use default value if key was deleted for ${storageArea}`); + browser.test.assertEq("test-value2", data.test2, + `Use stored value if found for ${storageArea}`); + + await storage.clear(); + data = await storage.get(); + browser.test.assertEq(0, Object.keys(data).length, + `Data is empty after clear for ${storageArea}`); + } + + browser.test.sendMessage("done"); + }, + // Note: when Android supports sync on the java layer we will need to add + // useAddonManager: "permanent" here. Bug 1625257 + manifest: { + permissions: ["storage"], + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_multiple.html b/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_multiple.html new file mode 100644 index 0000000000..d1bfbd824b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_multiple.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for multiple extensions trying to filterResponseData on the same request</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const TEST_URL = + "http://example.org/tests/toolkit/components/extensions/test/mochitest/file_streamfilter.txt"; + +add_task(async () => { + const firstExtension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "webRequestBlocking", "<all_urls>"], + }, + + background() { + browser.webRequest.onBeforeRequest.addListener( + ({ requestId }) => { + const filter = browser.webRequest.filterResponseData(requestId); + filter.ondata = event => { + filter.write(new TextEncoder().encode("Start ")); + filter.write(event.data); + filter.disconnect(); + }; + }, + { + urls: [ + "http://example.org/*/file_streamfilter.txt", + ], + }, + ["blocking"] + ); + }, + }); + + const secondExtension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "webRequestBlocking", "<all_urls>"], + }, + + background() { + browser.webRequest.onBeforeRequest.addListener( + ({ requestId }) => { + const filter = browser.webRequest.filterResponseData(requestId); + filter.ondata = event => { + filter.write(event.data); + }; + filter.onstop = event => { + filter.write(new TextEncoder().encode(" End")); + filter.close(); + }; + }, + { + urls: [ + "http://example.org/tests/toolkit/components/extensions/test/mochitest/file_streamfilter.txt", + ], + }, + ["blocking"] + ); + }, + }); + + await firstExtension.startup(); + await secondExtension.startup(); + + let iframe = document.createElement("iframe"); + iframe.src = TEST_URL; + document.body.appendChild(iframe); + await new Promise(resolve => iframe.addEventListener("load", () => resolve(), {once: true})); + + let content = await SpecialPowers.spawn(iframe, [], async () => { + return this.content.document.body.textContent; + }); + SimpleTest.is(content, "Start Middle\n End", "Correctly intercepted page content"); + + await firstExtension.unload(); + await secondExtension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_processswitch.html b/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_processswitch.html new file mode 100644 index 0000000000..2cf15db4e2 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_streamfilter_processswitch.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for using filterResponseData to intercept a cross-origin navigation that will involve a process switch with fission</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const TEST_HOST = "http://example.com/"; +const CROSS_ORIGIN_HOST = "http://example.org/"; +const TEST_PATH = + "tests/toolkit/components/extensions/test/mochitest/file_streamfilter.txt"; + +const TEST_URL = TEST_HOST + TEST_PATH; +const CROSS_ORIGIN_URL = CROSS_ORIGIN_HOST + TEST_PATH; + +add_task(async () => { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "webRequestBlocking", "<all_urls>"], + }, + + background() { + browser.webRequest.onBeforeRequest.addListener( + ({ requestId }) => { + const filter = browser.webRequest.filterResponseData(requestId); + filter.ondata = event => { + filter.write(event.data); + }; + filter.onstop = event => { + filter.write(new TextEncoder().encode(" End")); + filter.close(); + }; + }, + { + urls: [ + "http://example.org/*/file_streamfilter.txt", + ], + }, + ["blocking"] + ); + }, + }); + + await extension.startup(); + + let iframe = document.createElement("iframe"); + iframe.src = TEST_URL; + document.body.appendChild(iframe); + await new Promise(resolve => iframe.addEventListener("load", () => resolve(), {once: true})); + + + iframe.src = CROSS_ORIGIN_URL; + await new Promise(resolve => iframe.addEventListener("load", () => resolve(), {once: true})); + + let content = await SpecialPowers.spawn(iframe, [], async () => { + return this.content.document.body.textContent; + }); + SimpleTest.is(content, "Middle\n End", "Correctly intercepted page content"); + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html b/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html new file mode 100644 index 0000000000..f7389236ab --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html @@ -0,0 +1,340 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>WebExtension test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; +/* eslint-disable mozilla/balanced-listeners */ + +add_task(async function test_webext_tab_subframe_privileges() { + function background() { + browser.runtime.onMessage.addListener(async ({msg, success, tabId, error}) => { + if (msg == "webext-tab-subframe-privileges") { + if (success) { + await browser.tabs.remove(tabId); + + browser.test.notifyPass(msg); + } else { + browser.test.log(`Got an unexpected error: ${error}`); + + let tabs = await browser.tabs.query({active: true}); + await browser.tabs.remove(tabs[0].id); + + browser.test.notifyFail(msg); + } + } + }); + browser.tabs.create({url: browser.runtime.getURL("/tab.html")}); + } + + async function tabSubframeScript() { + browser.test.assertTrue(browser.tabs != undefined, + "Subframe of a privileged page has access to privileged APIs"); + if (browser.tabs) { + try { + let tab = await browser.tabs.getCurrent(); + browser.runtime.sendMessage({ + msg: "webext-tab-subframe-privileges", + success: true, + tabId: tab.id, + }); + } catch (e) { + browser.runtime.sendMessage({msg: "webext-tab-subframe-privileges", success: false, error: `${e}`}); + } + } else { + browser.runtime.sendMessage({ + msg: "webext-tab-subframe-privileges", + success: false, + error: `Privileged APIs missing in WebExtension tab sub-frame`, + }); + } + } + + let extensionData = { + background, + files: { + "tab.html": `<!DOCTYPE> + <head> + <meta charset="utf-8"> + </head> + <body> + <iframe src="tab-subframe.html"></iframe> + </body> + </html>`, + "tab-subframe.html": `<!DOCTYPE> + <head> + <meta charset="utf-8"> + <script src="tab-subframe.js"><\/script> + </head> + </html>`, + "tab-subframe.js": tabSubframeScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + + await extension.awaitFinish("webext-tab-subframe-privileges"); + await extension.unload(); +}); + +add_task(async function test_webext_background_subframe_privileges() { + function backgroundSubframeScript() { + browser.test.assertTrue(browser.tabs != undefined, + "Subframe of a background page has access to privileged APIs"); + browser.test.notifyPass("webext-background-subframe-privileges"); + } + + let extensionData = { + manifest: { + background: { + page: "background.html", + }, + }, + files: { + "background.html": `<!DOCTYPE> + <head> + <meta charset="utf-8"> + </head> + <body> + <iframe src="background-subframe.html"></iframe> + </body> + </html>`, + "background-subframe.html": `<!DOCTYPE> + <head> + <meta charset="utf-8"> + <script src="background-subframe.js"><\/script> + </head> + </html>`, + "background-subframe.js": backgroundSubframeScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + + await extension.awaitFinish("webext-background-subframe-privileges"); + await extension.unload(); +}); + +add_task(async function test_webext_contentscript_iframe_subframe_privileges() { + function background() { + browser.runtime.onMessage.addListener(({name, hasTabsAPI, hasStorageAPI}) => { + if (name == "contentscript-iframe-loaded") { + browser.test.assertFalse(hasTabsAPI, + "Subframe of a content script privileged iframes has no access to privileged APIs"); + browser.test.assertTrue(hasStorageAPI, + "Subframe of a content script privileged iframes has access to content script APIs"); + + browser.test.notifyPass("webext-contentscript-subframe-privileges"); + } + }); + } + + function subframeScript() { + browser.runtime.sendMessage({ + name: "contentscript-iframe-loaded", + hasTabsAPI: browser.tabs != undefined, + hasStorageAPI: browser.storage != undefined, + }); + } + + function contentScript() { + let iframe = document.createElement("iframe"); + iframe.setAttribute("src", browser.runtime.getURL("/contentscript-iframe.html")); + document.body.appendChild(iframe); + } + + let extensionData = { + background, + manifest: { + "permissions": ["storage"], + "content_scripts": [{ + "matches": ["http://example.com/*"], + "js": ["contentscript.js"], + }], + web_accessible_resources: [ + "contentscript-iframe.html", + ], + }, + files: { + "contentscript.js": contentScript, + "contentscript-iframe.html": `<!DOCTYPE> + <head> + <meta charset="utf-8"> + </head> + <body> + <iframe src="contentscript-iframe-subframe.html"></iframe> + </body> + </html>`, + "contentscript-iframe-subframe.html": `<!DOCTYPE> + <head> + <meta charset="utf-8"> + <script src="contentscript-iframe-subframe.js"><\/script> + </head> + </html>`, + "contentscript-iframe-subframe.js": subframeScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + + let win = window.open("http://example.com"); + + await extension.awaitFinish("webext-contentscript-subframe-privileges"); + + win.close(); + + await extension.unload(); +}); + +add_task(async function test_webext_background_remote_subframe_privileges() { + function backgroundSubframeScript() { + window.addEventListener("message", evt => { + browser.test.assertEq("http://mochi.test:8888", evt.origin, "postmessage origin ok"); + browser.test.assertFalse(evt.data.tabs, "remote frame cannot access webextension APIs"); + browser.test.assertEq("cookie=monster", evt.data.cookie, "Expected cookie value"); + browser.test.notifyPass("webext-background-subframe-privileges"); + }, {once: true}); + browser.cookies.set({url: "http://mochi.test:8888", name: "cookie", "value": "monster"}); + } + + let extensionData = { + manifest: { + permissions: ["cookies", "*://mochi.test/*", "tabs"], + background: { + page: "background.html", + }, + }, + files: { + "background.html": `<!DOCTYPE> + <head> + <meta charset="utf-8"> + <script src="background-subframe.js"><\/script> + </head> + <body> + <iframe src='${SimpleTest.getTestFileURL("file_remote_frame.html")}'></iframe> + </body> + </html>`, + "background-subframe.js": backgroundSubframeScript, + }, + }; + // Need remote webextensions to be able to load remote content from a background page. + if (!SpecialPowers.getBoolPref("extensions.webextensions.remote", true)) { + return; + } + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + + await extension.awaitFinish("webext-background-subframe-privileges"); + await extension.unload(); +}); + +// Test a moz-extension:// iframe inside a content iframe in an extension page. +add_task(async function test_sub_subframe_conduit_verified_env() { + let manifest = { + content_scripts: [{ + matches: ["http://mochi.test/*/file_sample.html"], + all_frames: true, + js: ["cs.js"], + }], + background: { + page: "background.html", + }, + web_accessible_resources: ["iframe.html"], + }; + + let files = { + "iframe.html": `<!DOCTYPE html><meta charset=utf-8> iframe`, + "cs.js"() { + // A compromised content sandbox shouldn't be able to trick the parent + // process into giving it extension privileges by sending false metadata. + async function faker(extensionId, envType) { + try { + let id = envType + "-xyz1234"; + let wgc = this.content.windowGlobalChild; + + let conduit = wgc.getActor("Conduits").openConduit({}, { + id, + envType, + extensionId, + query: ["CreateProxyContext"], + }); + + return await conduit.queryCreateProxyContext({ + childId: id, + extensionId, + envType: "addon_parent", + url: this.content.location.href, + viewType: "tab", + }); + } catch (e) { + return e.message; + } + } + + let iframe = document.createElement("iframe"); + iframe.src = browser.runtime.getURL("iframe.html"); + + iframe.onload = async () => { + for (let envType of ["content_child", "addon_child"]) { + let msg = await this.wrappedJSObject.SpecialPowers.spawn( + iframe, [browser.runtime.id, envType], faker); + browser.test.sendMessage(envType, msg); + } + }; + document.body.appendChild(iframe); + }, + "background.html": `<!DOCTYPE html> + <meta charset=utf-8> + <iframe src="${SimpleTest.getTestFileURL("file_sample.html")}"> + </iframe> + page + `, + }; + + async function expectErrors(ext, log) { + let err = await ext.awaitMessage("content_child"); + is(err, "Bad sender context envType: content_child"); + + err = await ext.awaitMessage("addon_child"); + is(err, "Unknown sender or wrong actor for recvCreateProxyContext"); + } + + let remote = SpecialPowers.getBoolPref("extensions.webextensions.remote"); + + let badProcess = { message: /Bad {[\w-]+} process: web/ }; + let badPrincipal = { message: /Bad {[\w-]+} principal: http/ }; + consoleMonitor.start(remote ? [badPrincipal, badProcess] : [badProcess]); + + let extension = ExtensionTestUtils.loadExtension({ manifest, files }); + await extension.startup(); + + if (remote) { + info("Need OOP to spoof from a web iframe inside background page."); + await expectErrors(extension); + } + + info("Try spoofing from the web process."); + let win = window.open("./file_sample.html"); + await expectErrors(extension); + win.close(); + + await extension.unload(); + await consoleMonitor.finished(); + info("Conduit creation logged correct exception(s)."); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_captureTab.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_captureTab.html new file mode 100644 index 0000000000..7feb1064ba --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_captureTab.html @@ -0,0 +1,301 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests tabs.captureTab and tabs.captureVisibleTab</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script type="text/javascript"> +"use strict"; + +async function runTest({ html, fullZoom, coords, rect, scale }) { + let url = `data:text/html,${encodeURIComponent(html)}#scroll`; + + async function background({ coords, rect, scale, method, fullZoom }) { + try { + // Wait for the page to load + await new Promise(resolve => { + browser.webNavigation.onCompleted.addListener( + () => resolve(), + {url: [{schemes: ["data"]}]}); + }); + + let [tab] = await browser.tabs.query({ + currentWindow: true, + active: true, + }); + + // TODO: Bug 1665429 - on mobile we ignore zoom for now + if (browser.tabs.setZoom) { + await browser.tabs.setZoom(tab.id, fullZoom ?? 1); + } + + let id = method === "captureVisibleTab" ? tab.windowId : tab.id; + + let [jpeg, png, ...pngs] = await Promise.all([ + browser.tabs[method](id, { format: "jpeg", quality: 95, rect, scale }), + browser.tabs[method](id, { format: "png", quality: 95, rect, scale }), + browser.tabs[method](id, { quality: 95, rect, scale }), + browser.tabs[method](id, { rect, scale }), + ]); + + browser.test.assertTrue( + pngs.every(url => url == png), + "All PNGs are identical" + ); + + browser.test.assertTrue( + jpeg.startsWith("data:image/jpeg;base64,"), + "jpeg is JPEG" + ); + browser.test.assertTrue( + png.startsWith("data:image/png;base64,"), + "png is PNG" + ); + + let promises = [jpeg, png].map( + url => + new Promise(resolve => { + let img = new Image(); + img.src = url; + img.onload = () => resolve(img); + }) + ); + + let width = (rect?.width ?? tab.width) * (scale ?? devicePixelRatio); + let height = (rect?.height ?? tab.height) * (scale ?? devicePixelRatio); + + [jpeg, png] = await Promise.all(promises); + let images = { jpeg, png }; + for (let format of Object.keys(images)) { + let img = images[format]; + + // WGP.drawSnapshot() deals in int coordinates, and rounds down. + browser.test.assertTrue( + Math.abs(width - img.width) <= 1, + `${format} ok image width: ${img.width}, expected: ${width}` + ); + browser.test.assertTrue( + Math.abs(height - img.height) <= 1, + `${format} ok image height ${img.height}, expected: ${height}` + ); + + let canvas = document.createElement("canvas"); + canvas.width = img.width; + canvas.height = img.height; + canvas.mozOpaque = true; + + let ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); + + for (let { x, y, color } of coords) { + x = (x + img.width) % img.width; + y = (y + img.height) % img.height; + let imageData = ctx.getImageData(x, y, 1, 1).data; + + if (format == "png") { + browser.test.assertEq( + `rgba(${color},255)`, + `rgba(${[...imageData]})`, + `${format} image color is correct at (${x}, ${y})` + ); + } else { + // Allow for some deviation in JPEG version due to lossy compression. + const SLOP = 3; + + browser.test.log( + `Testing ${format} image color at (${x}, ${y}), have rgba(${[ + ...imageData, + ]}), expecting approx. rgba(${color},255)` + ); + + browser.test.assertTrue( + Math.abs(color[0] - imageData[0]) <= SLOP, + `${format} image color.red is correct at (${x}, ${y})` + ); + browser.test.assertTrue( + Math.abs(color[1] - imageData[1]) <= SLOP, + `${format} image color.green is correct at (${x}, ${y})` + ); + browser.test.assertTrue( + Math.abs(color[2] - imageData[2]) <= SLOP, + `${format} image color.blue is correct at (${x}, ${y})` + ); + browser.test.assertEq( + 255, + imageData[3], + `${format} image color.alpha is correct at (${x}, ${y})` + ); + } + } + } + + browser.test.notifyPass("captureTab"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("captureTab"); + } + } + + for (let method of ["captureTab", "captureVisibleTab"]) { + let options = { coords, rect, scale, method, fullZoom }; + info(`Testing configuration: ${JSON.stringify(options)}`); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["<all_urls>", "webNavigation"], + }, + + background: `(${background})(${JSON.stringify(options)})`, + }); + + await extension.startup(); + + let testWindow = window.open(url); + await extension.awaitFinish("captureTab"); + + testWindow.close(); + await extension.unload(); + } +} + +async function testEdgeToEdge({ color, fullZoom }) { + let neutral = [0xaa, 0xaa, 0xaa]; + + let html = ` + <!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + </head> + <body style="background-color: rgb(${color})"> + <!-- Fill most of the image with a neutral color to test edge-to-edge scaling. --> + <div style="position: absolute; + left: 2px; + right: 2px; + top: 2px; + bottom: 2px; + background: rgb(${neutral});"></div> + </body> + </html> + `; + + // Check the colors of the first and last pixels of the image, to make + // sure we capture the entire frame, and scale it correctly. + let coords = [ + { x: 0, y: 0, color }, + { x: -1, y: -1, color }, + { x: 300, y: 200, color: neutral }, + ]; + + info(`Test edge to edge color ${color} at fullZoom=${fullZoom}`); + await runTest({ html, fullZoom, coords }); +} + +add_task(async function testCaptureEdgeToEdge() { + await testEdgeToEdge({ color: [0, 0, 0], fullZoom: 1 }); + await testEdgeToEdge({ color: [0, 0, 0], fullZoom: 2 }); + await testEdgeToEdge({ color: [0, 0, 0], fullZoom: 0.5 }); + await testEdgeToEdge({ color: [255, 255, 255], fullZoom: 1 }); +}); + +const tallDoc = `<!DOCTYPE html> + <meta charset=utf-8> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <div style="background: yellow; width: 50%; height: 500px;"></div> + <div id=scroll style="background: red; width: 25%; height: 5000px;"></div> + Opened with the #scroll fragment, scrolls the div ^ into view. +`; + +// Test currently visible viewport is captured if scrolling is involved. +add_task(async function testScrolledViewport() { + await runTest({ + html: tallDoc, + coords: [ + { x: 50, y: 50, color: [255, 0, 0] }, + { x: 50, y: -50, color: [255, 0, 0] }, + { x: -50, y: -50, color: [255, 255, 255] }, + ], + }); +}); + +// Test rect and scale options. +add_task(async function testRectAndScale() { + await runTest({ + html: tallDoc, + rect: { x: 50, y: 50, width: 10, height: 1000 }, + scale: 4, + coords: [ + { x: 0, y: 0, color: [255, 255, 0] }, + { x: -1, y: 0, color: [255, 255, 0] }, + { x: 0, y: -1, color: [255, 0, 0] }, + { x: -1, y: -1, color: [255, 0, 0] }, + ], + }); +}); + +// Test OOP iframes are captured, for Fission compatibility. +add_task(async function testOOPiframe() { + await runTest({ + html: `<!DOCTYPE html> + <meta charset=utf-8> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <iframe src="http://example.net/tests/toolkit/components/extensions/test/mochitest/file_green.html"></iframe> + `, + coords: [ + { x: 50, y: 50, color: [0, 255, 0] }, + { x: 50, y: -50, color: [255, 255, 255] }, + { x: -50, y: 50, color: [255, 255, 255] }, + ], + }); +}); + +add_task(async function testCaptureTabPermissions() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs"], + }, + + background() { + browser.test.assertEq( + undefined, + browser.tabs.captureTab, + 'Extension without "<all_urls>" permission should not have access to captureTab' + ); + browser.test.notifyPass("captureTabPermissions"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("captureTabPermissions"); + await extension.unload(); +}); + +add_task(async function testCaptureVisibleTabPermissions() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs"], + }, + + background() { + browser.test.assertEq( + undefined, + browser.tabs.captureVisibleTab, + 'Extension without "<all_urls>" permission should not have access to captureVisibleTab' + ); + browser.test.notifyPass("captureVisibleTabPermissions"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("captureVisibleTabPermissions"); + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_permissions.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_permissions.html new file mode 100644 index 0000000000..99d8b77f16 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_permissions.html @@ -0,0 +1,780 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Tabs permissions test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const URL1 = + "http://www.example.com/tests/toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html"; +const URL2 = + "http://example.net/tests/toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html"; + +const helperExtensionDef = { + manifest: { + applications: { + gecko: { + id: "helper@tests.mozilla.org", + }, + }, + permissions: ["webNavigation", "<all_urls>"], + }, + + useAddonManager: "permanent", + + async background() { + browser.test.onMessage.addListener(async message => { + switch (message.subject) { + case "createTab": { + const tabLoaded = new Promise(resolve => { + browser.webNavigation.onCompleted.addListener(function listener( + details + ) { + if (details.url === message.data.url) { + browser.webNavigation.onCompleted.removeListener(listener); + resolve(); + } + }); + }); + + const tab = await browser.tabs.create({ url: message.data.url }); + await tabLoaded; + browser.test.sendMessage("tabCreated", tab.id); + break; + } + + case "changeTabURL": { + const tabLoaded = new Promise(resolve => { + browser.webNavigation.onCompleted.addListener(function listener( + details + ) { + if (details.url === message.data.url) { + browser.webNavigation.onCompleted.removeListener(listener); + resolve(); + } + }); + }); + + await browser.tabs.update(message.data.tabId, { + url: message.data.url, + }); + await tabLoaded; + browser.test.sendMessage("tabURLChanged", message.data.tabId); + break; + } + + case "changeTabHashAndTitle": { + const tabChanged = new Promise(resolve => { + let hasURLChangeInfo = false, + hasTitleChangeInfo = false; + browser.tabs.onUpdated.addListener(function listener( + tabId, + changeInfo, + tab + ) { + if (changeInfo.url?.endsWith(message.data.urlHash)) { + hasURLChangeInfo = true; + } + if (changeInfo.title === message.data.title) { + hasTitleChangeInfo = true; + } + if (hasURLChangeInfo && hasTitleChangeInfo) { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + + await browser.tabs.executeScript(message.data.tabId, { + code: ` + document.location.hash = ${JSON.stringify(message.data.urlHash)}; + document.title = ${JSON.stringify(message.data.title)}; + `, + }); + await tabChanged; + browser.test.sendMessage("tabHashAndTitleChanged"); + break; + } + + case "removeTab": { + await browser.tabs.remove(message.data.tabId); + browser.test.sendMessage("tabRemoved"); + break; + } + + default: + browser.test.fail(`Received unexpected message: ${message}`); + } + }); + }, +}; + +/* + * Test tabs.query function + * Check if the correct tabs are queried by url or title based on the granted permissions + */ +async function test_query(testCases, permissions) { + const helperExtension = ExtensionTestUtils.loadExtension(helperExtensionDef); + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: "permissions@tests.mozilla.org", + }, + }, + permissions, + }, + + useAddonManager: "permanent", + + async background() { + // wait for start message + const [testCases, tabIdFromURL1, tabIdFromURL2] = await new Promise( + resolve => { + browser.test.onMessage.addListener(message => resolve(message)); + } + ); + + for (const testCase of testCases) { + const query = testCase.query; + const matchingTabs = testCase.matchingTabs; + + let tabQuery = await browser.tabs.query(query); + // ignore other tabs in the window + tabQuery = tabQuery.filter(tab => { + return tab.id === tabIdFromURL1 || tab.id === tabIdFromURL2; + }); + + browser.test.assertEq(matchingTabs, tabQuery.length, `Tabs queried`); + } + // send end message + browser.test.notifyPass("tabs.query"); + }, + }); + + await helperExtension.startup(); + await extension.startup(); + + helperExtension.sendMessage({ + subject: "createTab", + data: { url: URL1 }, + }); + const tabIdFromURL1 = await helperExtension.awaitMessage("tabCreated"); + + helperExtension.sendMessage({ + subject: "createTab", + data: { url: URL2 }, + }); + const tabIdFromURL2 = await helperExtension.awaitMessage("tabCreated"); + + if (permissions.includes("activeTab")) { + extension.grantActiveTab(tabIdFromURL2); + } + + extension.sendMessage([testCases, tabIdFromURL1, tabIdFromURL2]); + await extension.awaitFinish("tabs.query"); + + helperExtension.sendMessage({ + subject: "removeTab", + data: { tabId: tabIdFromURL1 }, + }); + await helperExtension.awaitMessage("tabRemoved"); + + helperExtension.sendMessage({ + subject: "removeTab", + data: { tabId: tabIdFromURL2 }, + }); + await helperExtension.awaitMessage("tabRemoved"); + + await extension.unload(); + await helperExtension.unload(); +} + +// http://www.example.com host permission +add_task(function query_with_host_permission_url1() { + return test_query( + [ + { + query: { url: "*://www.example.com/*" }, + matchingTabs: 1, + }, + { + query: { url: "<all_urls>" }, + matchingTabs: 1, + }, + { + query: { url: ["*://www.example.com/*", "*://example.net/*"] }, + matchingTabs: 1, + }, + { + query: { title: "The Title" }, + matchingTabs: 1, + }, + { + query: { title: "Another Title" }, + matchingTabs: 0, + }, + { + query: {}, + matchingTabs: 2, + }, + ], + ["*://www.example.com/*"] + ); +}); + +// http://example.net host permission +add_task(function query_with_host_permission_url2() { + return test_query( + [ + { + query: { url: "*://www.example.com/*" }, + matchingTabs: 0, + }, + { + query: { url: "<all_urls>" }, + matchingTabs: 1, + }, + { + query: { url: ["*://www.example.com/*", "*://example.net/*"] }, + matchingTabs: 1, + }, + { + query: { title: "The Title" }, + matchingTabs: 0, + }, + { + query: { title: "Another Title" }, + matchingTabs: 1, + }, + { + query: {}, + matchingTabs: 2, + }, + ], + ["*://example.net/*"] + ); +}); + +// <all_urls> permission +add_task(function query_with_host_permission_all_urls() { + return test_query( + [ + { + query: { url: "*://www.example.com/*" }, + matchingTabs: 1, + }, + { + query: { url: "<all_urls>" }, + matchingTabs: 2, + }, + { + query: { url: ["*://www.example.com/*", "*://example.net/*"] }, + matchingTabs: 2, + }, + { + query: { title: "The Title" }, + matchingTabs: 1, + }, + { + query: { title: "Another Title" }, + matchingTabs: 1, + }, + { + query: {}, + matchingTabs: 2, + }, + ], + ["<all_urls>"] + ); +}); + +// tabs permission +add_task(function query_with_tabs_permission() { + return test_query( + [ + { + query: { url: "*://www.example.com/*" }, + matchingTabs: 1, + }, + { + query: { url: "<all_urls>" }, + matchingTabs: 2, + }, + { + query: { url: ["*://www.example.com/*", "*://example.net/*"] }, + matchingTabs: 2, + }, + { + query: { title: "The Title" }, + matchingTabs: 1, + }, + { + query: { title: "Another Title" }, + matchingTabs: 1, + }, + { + query: {}, + matchingTabs: 2, + }, + ], + ["tabs"] + ); +}); + +// activeTab permission +add_task(function query_with_activeTab_permission() { + return test_query( + [ + { + query: { url: "*://www.example.com/*" }, + matchingTabs: 0, + }, + { + query: { url: "<all_urls>" }, + matchingTabs: 1, + }, + { + query: { url: ["*://www.example.com/*", "*://example.net/*"] }, + matchingTabs: 1, + }, + { + query: { title: "The Title" }, + matchingTabs: 0, + }, + { + query: { title: "Another Title" }, + matchingTabs: 1, + }, + { + query: {}, + matchingTabs: 2, + }, + ], + ["activeTab"] + ); +}); +// no permission +add_task(function query_without_permission() { + return test_query( + [ + { + query: { url: "*://www.example.com/*" }, + matchingTabs: 0, + }, + { + query: { url: "<all_urls>" }, + matchingTabs: 0, + }, + { + query: { url: ["*://www.example.com/*", "*://example.net/*"] }, + matchingTabs: 0, + }, + { + query: { title: "The Title" }, + matchingTabs: 0, + }, + { + query: { title: "Another Title" }, + matchingTabs: 0, + }, + { + query: {}, + matchingTabs: 2, + }, + ], + [] + ); +}); + +/* + * Test tabs.onUpdate and tabs.get function + * Check if the changeInfo or tab object contains the restricted properties + * url and title only when the right permissions are granted + * The tab is updated without causing navigation in order to also test activeTab permission + */ +async function test_restricted_properties( + permissions, + hasRestrictedProperties +) { + const helperExtension = ExtensionTestUtils.loadExtension(helperExtensionDef); + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: "permissions@tests.mozilla.org", + }, + }, + permissions, + }, + + useAddonManager: "permanent", + + async background() { + // wait for test start signal and data + const [ + hasRestrictedProperties, + tabId, + urlHash, + title, + ] = await new Promise(resolve => { + browser.test.onMessage.addListener(message => { + resolve(message); + }); + }); + + let hasURLChangeInfo = false, + hasTitleChangeInfo = false; + function onUpdateListener(tabId, changeInfo, tab) { + if (changeInfo.url?.endsWith(urlHash)) { + hasURLChangeInfo = true; + } + if (changeInfo.title === title) { + hasTitleChangeInfo = true; + } + } + browser.tabs.onUpdated.addListener(onUpdateListener); + + // wait for test evaluation signal and data + await new Promise(resolve => { + browser.test.onMessage.addListener(message => { + if (message === "collectTestResults") { + resolve(message); + } + }); + browser.test.sendMessage("waitingForTabPropertyChanges"); + }); + + // check onUpdate changeInfo + browser.test.assertEq( + hasRestrictedProperties, + hasURLChangeInfo, + `Has changeInfo property "url"` + ); + browser.test.assertEq( + hasRestrictedProperties, + hasTitleChangeInfo, + `Has changeInfo property "title"` + ); + // check tab properties + const tabGet = await browser.tabs.get(tabId); + browser.test.assertEq( + hasRestrictedProperties, + !!tabGet.url?.endsWith(urlHash), + `Has tab property "url"` + ); + browser.test.assertEq( + hasRestrictedProperties, + tabGet.title === title, + `Has tab property "title"` + ); + // send end message + browser.test.notifyPass("tabs.restricted_properties"); + }, + }); + + const urlHash = "#ChangedURL"; + const title = "Changed Title"; + + await helperExtension.startup(); + await extension.startup(); + + helperExtension.sendMessage({ + subject: "createTab", + data: { url: URL1 }, + }); + const tabId = await helperExtension.awaitMessage("tabCreated"); + + if (permissions.includes("activeTab")) { + extension.grantActiveTab(tabId); + } + // send test start signal and data + extension.sendMessage([hasRestrictedProperties, tabId, urlHash, title]); + await extension.awaitMessage("waitingForTabPropertyChanges"); + + helperExtension.sendMessage({ + subject: "changeTabHashAndTitle", + data: { + tabId, + urlHash, + title, + }, + }); + await helperExtension.awaitMessage("tabHashAndTitleChanged"); + + // send end signal and evaluate results + extension.sendMessage("collectTestResults"); + await extension.awaitFinish("tabs.restricted_properties"); + + helperExtension.sendMessage({ + subject: "removeTab", + data: { tabId }, + }); + await helperExtension.awaitMessage("tabRemoved"); + + await extension.unload(); + await helperExtension.unload(); +} + +// http://www.example.com host permission +add_task(function has_restricted_properties_with_host_permission_url1() { + return test_restricted_properties(["*://www.example.com/*"], true); +}); +// http://example.net host permission +add_task(function has_restricted_properties_with_host_permission_url2() { + return test_restricted_properties(["*://example.net/*"], false); +}); +// <all_urls> permission +add_task(function has_restricted_properties_with_host_permission_all_urls() { + return test_restricted_properties(["<all_urls>"], true); +}); +// tabs permission +add_task(function has_restricted_properties_with_tabs_permission() { + return test_restricted_properties(["tabs"], true); +}); +// activeTab permission +add_task(function has_restricted_properties_with_activeTab_permission() { + return test_restricted_properties(["activeTab"], true); +}).skip(); // TODO bug 1686080: support changeInfo.url with activeTab +// no permission +add_task(function has_restricted_properties_without_permission() { + return test_restricted_properties([], false); +}); + + +/* + * Test tabs.onUpdate filter functionality + * Check if the restricted filter properties only work if the + * right permissions are granted + */ +async function test_onUpdateFilter(testCases, permissions) { + // Filters for onUpdated are not supported on Android. + if (AppConstants.platform === "android") { + return; + } + + const helperExtension = ExtensionTestUtils.loadExtension(helperExtensionDef); + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: "permissions@tests.mozilla.org", + }, + }, + permissions, + }, + + useAddonManager: "permanent", + + async background() { + let listenerGotCalled = false; + function onUpdateListener(tabId, changeInfo, tab) { + listenerGotCalled = true; + } + + browser.test.onMessage.addListener(async message => { + switch (message.subject) { + case "setup": { + browser.tabs.onUpdated.addListener( + onUpdateListener, + message.data.filter + ); + browser.test.sendMessage("done"); + break; + } + + case "collectTestResults": { + browser.test.assertEq( + message.data.expectEvent, + listenerGotCalled, + `Update listener called` + ); + browser.tabs.onUpdated.removeListener(onUpdateListener); + listenerGotCalled = false; + browser.test.sendMessage("done"); + break; + } + + default: + browser.test.fail(`Received unexpected message: ${message}`); + } + }); + }, + }); + + await helperExtension.startup(); + await extension.startup(); + + for (const testCase of testCases) { + helperExtension.sendMessage({ + subject: "createTab", + data: { url: URL1 }, + }); + const tabId = await helperExtension.awaitMessage("tabCreated"); + + extension.sendMessage({ + subject: "setup", + data: { + filter: testCase.filter, + }, + }); + await extension.awaitMessage("done"); + + helperExtension.sendMessage({ + subject: "changeTabURL", + data: { + tabId, + url: URL2, + }, + }); + await helperExtension.awaitMessage("tabURLChanged"); + + extension.sendMessage({ + subject: "collectTestResults", + data: { + expectEvent: testCase.expectEvent, + }, + }); + await extension.awaitMessage("done"); + + helperExtension.sendMessage({ + subject: "removeTab", + data: { tabId }, + }); + await helperExtension.awaitMessage("tabRemoved"); + } + + await extension.unload(); + await helperExtension.unload(); +} + +// http://mozilla.org host permission +add_task(function onUpdateFilter_with_host_permission_url3() { + return test_onUpdateFilter( + [ + { + filter: { urls: ["*://mozilla.org/*"] }, + expectEvent: false, + }, + { + filter: { urls: ["<all_urls>"] }, + expectEvent: false, + }, + { + filter: { urls: ["*://mozilla.org/*", "*://example.net/*"] }, + expectEvent: false, + }, + { + filter: { properties: ["title"] }, + expectEvent: false, + }, + { + filter: {}, + expectEvent: true, + }, + ], + ["*://mozilla.org/*"] + ); +}); + +// http://example.net host permission +add_task(function onUpdateFilter_with_host_permission_url2() { + return test_onUpdateFilter( + [ + { + filter: { urls: ["*://mozilla.org/*"] }, + expectEvent: false, + }, + { + filter: { urls: ["<all_urls>"] }, + expectEvent: true, + }, + { + filter: { urls: ["*://mozilla.org/*", "*://example.net/*"] }, + expectEvent: true, + }, + { + filter: { properties: ["title"] }, + expectEvent: true, + }, + { + filter: {}, + expectEvent: true, + }, + ], + ["*://example.net/*"] + ); +}); + +// <all_urls> permission +add_task(function onUpdateFilter_with_host_permission_all_urls() { + return test_onUpdateFilter( + [ + { + filter: { urls: ["*://mozilla.org/*"] }, + expectEvent: false, + }, + { + filter: { urls: ["<all_urls>"] }, + expectEvent: true, + }, + { + filter: { urls: ["*://mozilla.org/*", "*://example.net/*"] }, + expectEvent: true, + }, + { + filter: { properties: ["title"] }, + expectEvent: true, + }, + { + filter: {}, + expectEvent: true, + }, + ], + ["<all_urls>"] + ); +}); + +// tabs permission +add_task(function onUpdateFilter_with_tabs_permission() { + return test_onUpdateFilter( + [ + { + filter: { urls: ["*://mozilla.org/*"] }, + expectEvent: false, + }, + { + filter: { urls: ["<all_urls>"] }, + expectEvent: true, + }, + { + filter: { urls: ["*://mozilla.org/*", "*://example.net/*"] }, + expectEvent: true, + }, + { + filter: { properties: ["title"] }, + expectEvent: true, + }, + { + filter: {}, + expectEvent: true, + }, + ], + ["tabs"] + ); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_query_popup.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_query_popup.html new file mode 100644 index 0000000000..6393114c5f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_query_popup.html @@ -0,0 +1,95 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Tabs create Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +async function test_query(query) { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: "current-window@tests.mozilla.org", + } + }, + permissions: ["tabs"], + browser_action: { + default_popup: "popup.html", + }, + }, + + useAddonManager: "permanent", + + background: async function() { + let query = await new Promise(resolve => { + browser.test.onMessage.addListener(message => { + resolve(message); + }); + }); + let tab = await browser.tabs.create({ url: "http://www.example.com", active: true }); + browser.runtime.onMessage.addListener(message => { + if (message === "popup-loaded") { + browser.runtime.sendMessage({ tab, query }); + } + }); + browser.test.withHandlingUserInput(() => + browser.browserAction.openPopup() + ); + }, + + files: { + "popup.html": `<!DOCTYPE html><meta charset="utf-8"><script src="popup.js"><\/script>`, + "popup.js"() { + browser.runtime.onMessage.addListener(async function({ tab, query }) { + let tabs = await browser.tabs.query(query); + browser.test.assertEq(tabs.length, 1, `Got one tab`); + browser.test.assertEq(tabs[0].id, tab.id, "The tab is the right one"); + + // Create a new tab and verify that we still see the right result + let newTab = await browser.tabs.create({ url: "http://www.example.com", active: true }); + tabs = await browser.tabs.query(query); + browser.test.assertEq(tabs.length, 1, `Got one tab`); + browser.test.assertEq(tabs[0].id, newTab.id, "Got the newly-created tab"); + + await browser.tabs.remove(newTab.id); + + // Remove the tab and verify that we see the old tab + tabs = await browser.tabs.query(query); + browser.test.assertEq(tabs.length, 1, `Got one tab`); + browser.test.assertEq(tabs[0].id, tab.id, "Got the tab that was active before"); + + // Cleanup + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("tabs.query"); + }); + browser.runtime.sendMessage("popup-loaded"); + }, + }, + }); + + await extension.startup(); + extension.sendMessage(query); + await extension.awaitFinish("tabs.query"); + await extension.unload(); +} + +add_task(function test_query_currentWindow_from_popup() { + return test_query({ currentWindow: true, active: true }); +}); + +add_task(function test_query_lastActiveWindow_from_popup() { + return test_query({ lastFocusedWindow: true, active: true }); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html new file mode 100644 index 0000000000..293914fe5d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html @@ -0,0 +1,95 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test tabs.sendMessage</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script> +"use strict"; + +add_task(async function test_tabs_sendMessage_to_extension_page_frame() { + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + applications: { + gecko: { id: "blah@android" }, + }, + content_scripts: [{ + matches: ["http://mochi.test/*/file_sample.html?tabs.sendMessage"], + js: ["cs.js"], + }], + web_accessible_resources: ["page.html", "page.js"], + }, + + async background() { + let tab; + + browser.runtime.onMessage.addListener(async (msg, sender) => { + browser.test.assertEq(msg, "page-script-ready"); + browser.test.assertEq(sender.url, browser.runtime.getURL("page.html")); + + let tabId = sender.tab.id; + let response = await browser.tabs.sendMessage(tabId, "tab-sendMessage"); + + switch (response) { + case "extension-tab": + browser.test.assertEq(tab.id, tabId, "Extension tab responded"); + browser.test.assertEq(sender.frameId, 0, "Response from top level"); + await browser.tabs.remove(tab.id); + browser.test.sendMessage("extension-tab-responded"); + break; + + case "extension-frame": + browser.test.assertTrue(sender.frameId > 0, "Response from iframe"); + browser.test.sendMessage("extension-frame-responded"); + break; + + default: + browser.test.fail("Unexpected response: " + response); + } + }); + + tab = await browser.tabs.create({ url: "page.html" }); + }, + + files: { + "cs.js"() { + let iframe = document.createElement("iframe"); + iframe.src = browser.runtime.getURL("page.html"); + document.body.append(iframe); + browser.test.sendMessage("content-script-done"); + }, + + "page.html": `<!DOCTYPE html> + <meta charset=utf-8> + <script src=page.js><\/script> + Extension page`, + + "page.js"() { + browser.runtime.onMessage.addListener(async msg => { + browser.test.assertEq(msg, "tab-sendMessage"); + return window.parent === window ? "extension-tab" : "extension-frame"; + }); + browser.runtime.sendMessage("page-script-ready"); + }, + } + }); + + await extension.startup(); + await extension.awaitMessage("extension-tab-responded"); + + let win = window.open("file_sample.html?tabs.sendMessage"); + await extension.awaitMessage("content-script-done"); + await extension.awaitMessage("extension-frame-responded"); + win.close(); + + await extension.unload(); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_test.html b/toolkit/components/extensions/test/mochitest/test_ext_test.html new file mode 100644 index 0000000000..9fef13d8d4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_test.html @@ -0,0 +1,196 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Testing test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> + +<script> +"use strict"; + +function loadExtensionAndInterceptTest(extensionData) { + let results = []; + let testResolve; + let testDone = new Promise(resolve => { testResolve = resolve; }); + let handler = { + testResult(...result) { + result.pop(); + results.push(result); + SimpleTest.info(`Received test result: ${JSON.stringify(result)}`); + }, + + testMessage(msg, ...args) { + results.push(["test-message", msg, ...args]); + SimpleTest.info(`Received message: ${msg} ${JSON.stringify(args)}`); + if (msg === "This is the last browser.test call") { + testResolve(); + } + }, + }; + let extension = SpecialPowers.loadExtension(extensionData, handler); + SimpleTest.registerCleanupFunction(() => { + if (extension.state == "pending" || extension.state == "running") { + SimpleTest.ok(false, "Extension left running at test shutdown"); + return extension.unload(); + } else if (extension.state == "unloading") { + SimpleTest.ok(false, "Extension not fully unloaded at test shutdown"); + } + }); + extension.awaitResults = () => testDone.then(() => results); + return extension; +} + +function testScript() { + // Note: The result of these browser.test calls are intercepted by the test. + // See verifyTestResults for the expectations of each browser.test call. + browser.test.notifyPass("dot notifyPass"); + browser.test.notifyFail("dot notifyFail"); + browser.test.log("dot log"); + browser.test.fail("dot fail"); + browser.test.succeed("dot succeed"); + browser.test.assertTrue(true); + browser.test.assertFalse(false); + browser.test.assertEq("", ""); + + let obj = {}; + let arr = []; + let dom = document.createElement("body"); + browser.test.assertTrue(obj, "Object truthy"); + browser.test.assertTrue(arr, "Array truthy"); + browser.test.assertTrue(dom, "Element truthy"); + browser.test.assertTrue(true, "True truthy"); + browser.test.assertTrue(false, "False truthy"); + browser.test.assertTrue(null, "Null truthy"); + browser.test.assertTrue(undefined, "Void truthy"); + browser.test.assertTrue(false, document.createElement("html")); + + browser.test.assertFalse(obj, "Object falsey"); + browser.test.assertFalse(arr, "Array falsey"); + browser.test.assertFalse(dom, "Element falsey"); + browser.test.assertFalse(true, "True falsey"); + browser.test.assertFalse(false, "False falsey"); + browser.test.assertFalse(null, "Null falsey"); + browser.test.assertFalse(undefined, "Void falsey"); + browser.test.assertFalse(true, document.createElement("head")); + + browser.test.assertEq(obj, obj, "Object equality"); + browser.test.assertEq(arr, arr, "Array equality"); + browser.test.assertEq(dom, dom, "Element equality"); + browser.test.assertEq(null, null, "Null equality"); + browser.test.assertEq(undefined, undefined, "Void equality"); + + browser.test.assertEq({}, {}, "Object reference ineqality"); + browser.test.assertEq([], [], "Array reference ineqality"); + browser.test.assertEq(dom, document.createElement("body"), "Element ineqality"); + browser.test.assertEq(null, undefined, "Null and void ineqality"); + browser.test.assertEq(true, false, document.createElement("div")); + + obj = { + toString() { + return "Dynamic toString forbidden"; + }, + }; + browser.test.assertEq(obj, obj, "obj with dynamic toString()"); + browser.test.assertThrows( + () => { throw new Error("dummy"); }, + /dummy2/, + "intentional failure" + ); + browser.test.sendMessage("Ran test at", location.protocol); + browser.test.sendMessage("This is the last browser.test call"); +} + +function verifyTestResults(results, shortName, expectedProtocol) { + let expectations = [ + ["test-done", true, "dot notifyPass"], + ["test-done", false, "dot notifyFail"], + ["test-log", true, "dot log"], + ["test-result", false, "dot fail"], + ["test-result", true, "dot succeed"], + ["test-result", true, "undefined"], + ["test-result", true, "undefined"], + ["test-eq", true, "undefined", "", ""], + + ["test-result", true, "Object truthy"], + ["test-result", true, "Array truthy"], + ["test-result", true, "Element truthy"], + ["test-result", true, "True truthy"], + ["test-result", false, "False truthy"], + ["test-result", false, "Null truthy"], + ["test-result", false, "Void truthy"], + ["test-result", false, "[object HTMLHtmlElement]"], + + ["test-result", false, "Object falsey"], + ["test-result", false, "Array falsey"], + ["test-result", false, "Element falsey"], + ["test-result", false, "True falsey"], + ["test-result", true, "False falsey"], + ["test-result", true, "Null falsey"], + ["test-result", true, "Void falsey"], + ["test-result", false, "[object HTMLHeadElement]"], + + ["test-eq", true, "Object equality", "[object Object]", "[object Object]"], + ["test-eq", true, "Array equality", "", ""], + ["test-eq", true, "Element equality", "[object HTMLBodyElement]", "[object HTMLBodyElement]"], + ["test-eq", true, "Null equality", "null", "null"], + ["test-eq", true, "Void equality", "undefined", "undefined"], + + ["test-eq", false, "Object reference ineqality", "[object Object]", "[object Object] (different)"], + ["test-eq", false, "Array reference ineqality", "", " (different)"], + ["test-eq", false, "Element ineqality", "[object HTMLBodyElement]", "[object HTMLBodyElement] (different)"], + ["test-eq", false, "Null and void ineqality", "null", "undefined"], + ["test-eq", false, "[object HTMLDivElement]", "true", "false"], + + ["test-eq", true, "obj with dynamic toString()", "[object Object]", "[object Object]"], + ["test-result", false, "Function threw, expecting error to match /dummy2/, got \"dummy\": intentional failure"], + + ["test-message", "Ran test at", expectedProtocol], + ["test-message", "This is the last browser.test call"], + ]; + + expectations.forEach((expectation, i) => { + let msg = expectation.slice(2).join(" - "); + isDeeply(results[i], expectation, `${shortName} (${msg})`); + }); + is(results[expectations.length], undefined, "No more results"); +} + +add_task(async function test_test_in_background() { + let extensionData = { + background: `(${testScript})()`, + }; + + let extension = loadExtensionAndInterceptTest(extensionData); + await extension.startup(); + let results = await extension.awaitResults(); + verifyTestResults(results, "background page", "moz-extension:"); + await extension.unload(); +}); + +add_task(async function test_test_in_content_script() { + let extensionData = { + manifest: { + content_scripts: [{ + matches: ["http://mochi.test/*/file_sample.html"], + js: ["contentscript.js"], + }], + }, + files: { + "contentscript.js": `(${testScript})()`, + }, + }; + + let extension = loadExtensionAndInterceptTest(extensionData); + await extension.startup(); + let win = window.open("file_sample.html"); + let results = await extension.awaitResults(); + win.close(); + verifyTestResults(results, "content script", "http:"); + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage.html b/toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage.html new file mode 100644 index 0000000000..b92de8ab4f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage.html @@ -0,0 +1,139 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <script type="text/javascript" src="head_unlimitedStorage.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> + +"use strict"; + +async function test_background_storagePersist(EXTENSION_ID) { + await SpecialPowers.pushPrefEnv({ + "set": [ + ["dom.storageManager.enabled", true], + ["dom.storageManager.prompt.testing", false], + ["dom.storageManager.prompt.testing.allow", false], + ], + }); + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + + manifest: { + permissions: ["storage", "unlimitedStorage"], + applications: { + gecko: { + id: EXTENSION_ID, + }, + }, + }, + + background: async function() { + const PROMISE_RACE_TIMEOUT = 8000; + + browser.test.sendMessage("extension-uuid", window.location.host); + + await browser.storage.local.set({testkey: "testvalue"}); + await browser.test.sendMessage("storage-local-called"); + + const requestStoragePersist = async () => { + const persistAllowed = await navigator.storage.persist(); + if (!persistAllowed) { + throw new Error("navigator.storage.persist() has been denied"); + } + }; + + await Promise.race([ + requestStoragePersist(), + new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error("Timeout opening persistent db from background page")); + }, PROMISE_RACE_TIMEOUT); + }), + ]).then( + () => { + browser.test.notifyPass("indexeddb-storagePersistent-unlimitedStorage-done"); + }, + (error) => { + browser.test.fail(`error while testing persistent IndexedDB storage: ${error}`); + browser.test.notifyFail("indexeddb-storagePersistent-unlimitedStorage-done"); + } + ); + }, + }); + + await extension.startup(); + + const uuid = await extension.awaitMessage("extension-uuid"); + + await extension.awaitMessage("storage-local-called"); + + let chromeScript = SpecialPowers.loadChromeScript(function test_country_data() { + const {addMessageListener, sendAsyncMessage} = this; + + addMessageListener("getPersistedStatus", (uuid) => { + const { + ExtensionStorageIDB, + } = ChromeUtils.import("resource://gre/modules/ExtensionStorageIDB.jsm"); + + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + + const {WebExtensionPolicy} = Cu.getGlobalForObject(ExtensionStorageIDB); + const policy = WebExtensionPolicy.getByHostname(uuid); + const storagePrincipal = ExtensionStorageIDB.getStoragePrincipal(policy.extension); + const request = Services.qms.persisted(storagePrincipal); + request.callback = () => { + // request.result will be undeinfed if the request failed (request.resultCode !== Cr.NS_OK). + sendAsyncMessage("gotPersistedStatus", request.result); + }; + }); + }); + + const persistedPromise = chromeScript.promiseOneMessage("gotPersistedStatus"); + chromeScript.sendAsyncMessage("getPersistedStatus", uuid); + is(await persistedPromise, true, "Got the expected persist status for the storagePrincipal"); + + await extension.awaitFinish("indexeddb-storagePersistent-unlimitedStorage-done"); + await extension.unload(); + + checkSitePermissions(uuid, Services.perms.UNKNOWN_ACTION, "has been cleared"); +} + +add_task(async function test_unlimitedStorage() { + const EXTENSION_ID = "test-storagePersist@mozilla"; + await SpecialPowers.pushPrefEnv({ + "set": [ + ["extensions.webextensions.ExtensionStorageIDB.enabled", true], + ], + }); + + // Verify persist mode enabled when the storage.local IDB database is opened from + // the main process (from parent/ext-storage.js). + info("Test unlimitedStorage on an extension migrating to the IndexedDB storage.local backend)"); + await test_background_storagePersist(EXTENSION_ID); + + await SpecialPowers.pushPrefEnv({ + "set": [ + [`extensions.webextensions.ExtensionStorageIDB.migrated.` + EXTENSION_ID, true], + ], + }); + + // Verify persist mode enabled when the storage.local IDB database is opened from + // the child process (from child/ext-storage.js). + info("Test unlimitedStorage on an extension migrated to the IndexedDB storage.local backend"); + await test_background_storagePersist(EXTENSION_ID); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage_legacy_persistent_indexedDB.html b/toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage_legacy_persistent_indexedDB.html new file mode 100644 index 0000000000..fe06d22e8d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage_legacy_persistent_indexedDB.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <script type="text/javascript" src="head_unlimitedStorage.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> + +"use strict"; + +add_task(async function test_legacy_indexedDB_storagePersistent_unlimitedStorage() { + const EXTENSION_ID = "test-idbStoragePersistent@mozilla"; + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + + manifest: { + permissions: ["unlimitedStorage"], + applications: { + gecko: { + id: EXTENSION_ID, + }, + }, + }, + + background: async function() { + const PROMISE_RACE_TIMEOUT = 8000; + + browser.test.sendMessage("extension-uuid", window.location.host); + + try { + await Promise.race([ + new Promise((resolve, reject) => { + const dbReq = indexedDB.open("test-persistent-idb", {version: 1.0, storage: "persistent"}); + + dbReq.onerror = evt => { + reject(evt.target.error); + }; + + dbReq.onsuccess = () => { + resolve(); + }; + }), + new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error("Timeout opening persistent db from background page")); + }, PROMISE_RACE_TIMEOUT); + }), + ]); + + browser.test.notifyPass("indexeddb-storagePersistent-unlimitedStorage-done"); + } catch (error) { + const loggedError = error instanceof DOMException ? error.message : error; + browser.test.fail(`error while testing persistent IndexedDB storage: ${loggedError}`); + browser.test.notifyFail("indexeddb-storagePersistent-unlimitedStorage-done"); + } + }, + }); + + await extension.startup(); + + const uuid = await extension.awaitMessage("extension-uuid"); + + await extension.awaitFinish("indexeddb-storagePersistent-unlimitedStorage-done"); + + await extension.unload(); + + checkSitePermissions(uuid, Services.perms.UNKNOWN_ACTION, "has been cleared"); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_incognito.html b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_incognito.html new file mode 100644 index 0000000000..5c9de814e4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_incognito.html @@ -0,0 +1,174 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the web_accessible_resources incognito</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +let image = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="); +const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer; + +async function testImageLoading(src, expectedAction) { + let imageLoadingPromise = new Promise((resolve, reject) => { + let cleanupListeners; + let testImage = new window.Image(); + // Set the src via wrappedJSObject so the load is triggered with the + // content page's principal rather than ours. + testImage.wrappedJSObject.setAttribute("src", src); + + let loadListener = () => { + cleanupListeners(); + resolve(expectedAction === "loaded"); + }; + + let errorListener = (event) => { + cleanupListeners(); + resolve(expectedAction === "blocked"); + browser.test.log(`+++ image loading ${event.error}`); + }; + + cleanupListeners = () => { + testImage.removeEventListener("load", loadListener); + testImage.removeEventListener("error", errorListener); + }; + + testImage.addEventListener("load", loadListener); + testImage.addEventListener("error", errorListener); + }); + + let success = await imageLoadingPromise; + browser.runtime.sendMessage({name: "image-loading", expectedAction, success}); +} + +function testScript() { + window.postMessage("test-script-loaded", "*"); +} + +add_task(async function test_web_accessible_resources_incognito() { + await SpecialPowers.pushPrefEnv({set: [ + ["extensions.allowPrivateBrowsingByDefault", false], + ]}); + + // This extension will not have access to private browsing so its + // accessible resources should not be able to load in them. + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "web_accessible_resources": [ + "image.png", + "test_script.js", + "accessible.html", + ], + }, + background() { + browser.test.sendMessage("url", browser.extension.getURL("")); + }, + files: { + "image.png": IMAGE_ARRAYBUFFER, + "test_script.js": testScript, + "accessible.html": `<html><head> + <meta charset="utf-8"> + </head></html>`, + }, + }); + + await extension.startup(); + let baseUrl = await extension.awaitMessage("url"); + + async function content() { + let baseUrl = await browser.runtime.sendMessage({name: "get-url"}); + testImageLoading(`${baseUrl}image.png`, "loaded"); + + let testScriptElement = document.createElement("script"); + // Set the src via wrappedJSObject so the load is triggered with the + // content page's principal rather than ours. + testScriptElement.wrappedJSObject.setAttribute("src", `${baseUrl}test_script.js`); + document.head.appendChild(testScriptElement); + + let iframe = document.createElement("iframe"); + // Set the src via wrappedJSObject so the load is triggered with the + // content page's principal rather than ours. + iframe.wrappedJSObject.setAttribute("src", `${baseUrl}accessible.html`); + document.body.appendChild(iframe); + + // eslint-disable-next-line mozilla/balanced-listeners + window.addEventListener("message", event => { + browser.runtime.sendMessage({"name": event.data}); + }); + } + + let pb_extension = ExtensionTestUtils.loadExtension({ + incognitoOverride: "spanning", + manifest: { + permissions: ["tabs"], + content_scripts: [{ + "matches": ["*://example.com/*/file_sample.html"], + "run_at": "document_end", + "js": ["content_script_helper.js", "content_script.js"], + }], + }, + files: { + "content_script_helper.js": `${testImageLoading}`, + "content_script.js": content, + }, + background() { + let url = "http://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"; + let baseUrl; + let window; + + browser.runtime.onMessage.addListener(async msg => { + switch (msg.name) { + case "image-loading": + browser.test.assertFalse(msg.success, `Image was ${msg.expectedAction}`); + browser.test.sendMessage(`image-${msg.expectedAction}`); + break; + case "get-url": + return baseUrl; + default: + browser.test.fail(`unexepected message ${msg.name}`); + } + }); + + browser.test.onMessage.addListener(async (msg, data) => { + if (msg == "start") { + baseUrl = data; + window = await browser.windows.create({url, incognito: true}); + } + if (msg == "close") { + browser.windows.remove(window.id); + } + }); + }, + }); + await pb_extension.startup(); + + consoleMonitor.start([ + {message: /may not load or link to.*image.png/}, + {message: /may not load or link to.*test_script.js/}, + {message: /\<script\> source URI is not allowed in this document/}, + {message: /may not load or link to.*accessible.html/}, + ]); + + pb_extension.sendMessage("start", baseUrl); + + await pb_extension.awaitMessage("image-loaded"); + + pb_extension.sendMessage("close"); + + await extension.unload(); + await pb_extension.unload(); + + await consoleMonitor.finished(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html new file mode 100644 index 0000000000..d6ae4358d4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html @@ -0,0 +1,265 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the web_accessible_resources manifest directive</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +/* eslint-disable mozilla/balanced-listeners */ + +SimpleTest.registerCleanupFunction(() => { + SpecialPowers.clearUserPref("security.mixed_content.block_display_content"); +}); + +let image = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="); +const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer; + +async function testImageLoading(src, expectedAction) { + let imageLoadingPromise = new Promise((resolve, reject) => { + let cleanupListeners; + let testImage = document.createElement("img"); + // Set the src via wrappedJSObject so the load is triggered with the + // content page's principal rather than ours. + testImage.wrappedJSObject.setAttribute("src", src); + + let loadListener = () => { + cleanupListeners(); + resolve(expectedAction === "loaded"); + }; + + let errorListener = () => { + cleanupListeners(); + resolve(expectedAction === "blocked"); + }; + + cleanupListeners = () => { + testImage.removeEventListener("load", loadListener); + testImage.removeEventListener("error", errorListener); + }; + + testImage.addEventListener("load", loadListener); + testImage.addEventListener("error", errorListener); + + document.body.appendChild(testImage); + }); + + let success = await imageLoadingPromise; + browser.runtime.sendMessage({name: "image-loading", expectedAction, success}); +} + +add_task(async function test_web_accessible_resources() { + function background() { + let gotURL; + let tabId; + + function loadFrame(url) { + return new Promise(resolve => { + browser.tabs.sendMessage(tabId, ["load-iframe", url], reply => { + resolve(reply); + }); + }); + } + + let urls = [ + [browser.extension.getURL("accessible.html"), true], + [browser.extension.getURL("accessible.html") + "?foo=bar", true], + [browser.extension.getURL("accessible.html") + "#!foo=bar", true], + [browser.extension.getURL("forbidden.html"), false], + [browser.extension.getURL("wild1.html"), true], + [browser.extension.getURL("wild2.htm"), false], + ]; + + async function runTests() { + for (let [url, shouldLoad] of urls) { + let success = await loadFrame(url); + + browser.test.assertEq(shouldLoad, success, "Load was successful"); + if (shouldLoad) { + browser.test.assertEq(url, gotURL, "Got expected url"); + } else { + browser.test.assertEq(undefined, gotURL, "Got no url"); + } + gotURL = undefined; + } + + browser.test.notifyPass("web-accessible-resources"); + } + + browser.runtime.onMessage.addListener(([msg, url], sender) => { + if (msg == "content-script-ready") { + tabId = sender.tab.id; + runTests(); + } else if (msg == "page-script") { + browser.test.assertEq(undefined, gotURL, "Should have gotten only one message"); + browser.test.assertEq("string", typeof(url), "URL should be a string"); + gotURL = url; + } + }); + + browser.test.sendMessage("ready"); + } + + function contentScript() { + browser.runtime.onMessage.addListener(([msg, url], sender, respond) => { + if (msg == "load-iframe") { + let iframe = document.createElement("iframe"); + // Set the src via wrappedJSObject so the load is triggered with the + // content page's principal rather than ours. + iframe.wrappedJSObject.setAttribute("src", url); + iframe.addEventListener("load", () => { respond(true); }); + iframe.addEventListener("error", () => { respond(false); }); + document.body.appendChild(iframe); + return true; + } + }); + browser.runtime.sendMessage(["content-script-ready"]); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + "matches": ["http://example.com/"], + "js": ["content_script.js"], + "run_at": "document_idle", + }, + ], + + "web_accessible_resources": [ + "/accessible.html", + "wild*.html", + ], + }, + + background, + + files: { + "content_script.js": contentScript, + + "accessible.html": `<html><head> + <meta charset="utf-8"> + <script src="accessible.js"><\/script> + </head></html>`, + + "accessible.js": 'browser.runtime.sendMessage(["page-script", location.href]);', + + "inaccessible.html": `<html><head> + <meta charset="utf-8"> + <script src="inaccessible.js"><\/script> + </head></html>`, + + "inaccessible.js": 'browser.runtime.sendMessage(["page-script", location.href]);', + + "wild1.html": `<html><head> + <meta charset="utf-8"> + <script src="wild.js"><\/script> + </head></html>`, + + "wild2.htm": `<html><head> + <meta charset="utf-8"> + <script src="wild.js"><\/script> + </head></html>`, + + "wild.js": 'browser.runtime.sendMessage(["page-script", location.href]);', + }, + }); + + await extension.startup(); + + await extension.awaitMessage("ready"); + + let win = window.open("http://example.com/"); + + await extension.awaitFinish("web-accessible-resources"); + + win.close(); + + await extension.unload(); +}); + +add_task(async function test_web_accessible_resources_mixed_content() { + function background() { + browser.runtime.onMessage.addListener(msg => { + if (msg.name === "image-loading") { + browser.test.assertTrue(msg.success, `Image was ${msg.expectedAction}`); + browser.test.sendMessage(`image-${msg.expectedAction}`); + } else { + browser.test.sendMessage(msg); + if (msg === "accessible-script-loaded") { + browser.test.notifyPass("mixed-test"); + } + } + }); + + browser.test.sendMessage("background-ready"); + } + + function content() { + testImageLoading("http://example.com/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png", "blocked"); + testImageLoading(browser.extension.getURL("image.png"), "loaded"); + + let testScriptElement = document.createElement("script"); + // Set the src via wrappedJSObject so the load is triggered with the + // content page's principal rather than ours. + testScriptElement.wrappedJSObject.setAttribute("src", browser.extension.getURL("test_script.js")); + document.head.appendChild(testScriptElement); + + window.addEventListener("message", event => { + browser.runtime.sendMessage(event.data); + }); + } + + function testScript() { + window.postMessage("accessible-script-loaded", "*"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "content_scripts": [{ + "matches": ["https://example.com/*/file_mixed.html"], + "run_at": "document_end", + "js": ["content_script_helper.js", "content_script.js"], + }], + "web_accessible_resources": [ + "image.png", + "test_script.js", + ], + }, + background, + files: { + "content_script_helper.js": `${testImageLoading}`, + "content_script.js": content, + "test_script.js": testScript, + "image.png": IMAGE_ARRAYBUFFER, + }, + }); + + SpecialPowers.setBoolPref("security.mixed_content.block_display_content", true); + + await Promise.all([extension.startup(), extension.awaitMessage("background-ready")]); + + let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_mixed.html"); + + await Promise.all([ + extension.awaitMessage("image-blocked"), + extension.awaitMessage("image-loaded"), + extension.awaitMessage("accessible-script-loaded"), + ]); + await extension.awaitFinish("mixed-test"); + win.close(); + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html new file mode 100644 index 0000000000..f471ef6a2f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html @@ -0,0 +1,611 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +if (AppConstants.platform === "android") { + SimpleTest.requestLongerTimeout(3); +} + +/* globals sendMouseEvent */ + +function backgroundScript() { + const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; + const URL = BASE + "/file_WebNavigation_page1.html"; + + const EVENTS = [ + "onTabReplaced", + "onBeforeNavigate", + "onCommitted", + "onDOMContentLoaded", + "onCompleted", + "onErrorOccurred", + "onReferenceFragmentUpdated", + "onHistoryStateUpdated", + ]; + + let expectedTabId = -1; + + function gotEvent(event, details) { + if (!details.url.startsWith(BASE)) { + return; + } + browser.test.log(`Got ${event} ${details.url} ${details.frameId} ${details.parentFrameId}`); + + if (expectedTabId == -1) { + browser.test.assertTrue(details.tabId !== undefined, "tab ID defined"); + expectedTabId = details.tabId; + } + + browser.test.assertEq(details.tabId, expectedTabId, "correct tab"); + + browser.test.sendMessage("received", {url: details.url, event}); + + if (details.url == URL) { + browser.test.assertEq(0, details.frameId, "root frame ID correct"); + browser.test.assertEq(-1, details.parentFrameId, "root parent frame ID correct"); + } else { + browser.test.assertEq(0, details.parentFrameId, "parent frame ID correct"); + browser.test.assertTrue(details.frameId != 0, "frame ID probably okay"); + } + + browser.test.assertTrue(details.frameId !== undefined, "frameId != undefined"); + browser.test.assertTrue(details.parentFrameId !== undefined, "parentFrameId != undefined"); + } + + let listeners = {}; + for (let event of EVENTS) { + listeners[event] = gotEvent.bind(null, event); + browser.webNavigation[event].addListener(listeners[event]); + } + + browser.test.sendMessage("ready"); +} + +const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; +const URL = BASE + "/file_WebNavigation_page1.html"; +const FORM_URL = URL + "?"; +const FRAME = BASE + "/file_WebNavigation_page2.html"; +const FRAME2 = BASE + "/file_WebNavigation_page3.html"; +const FRAME_PUSHSTATE = BASE + "/file_WebNavigation_page3_pushState.html"; +const REDIRECT = BASE + "/redirection.sjs"; +const REDIRECTED = BASE + "/dummy_page.html"; +const CLIENT_REDIRECT = BASE + "/file_webNavigation_clientRedirect.html"; +const CLIENT_REDIRECT_HTTPHEADER = BASE + "/file_webNavigation_clientRedirect_httpHeaders.html"; +const FRAME_CLIENT_REDIRECT = BASE + "/file_webNavigation_frameClientRedirect.html"; +const FRAME_REDIRECT = BASE + "/file_webNavigation_frameRedirect.html"; +const FRAME_MANUAL = BASE + "/file_webNavigation_manualSubframe.html"; +const FRAME_MANUAL_PAGE1 = BASE + "/file_webNavigation_manualSubframe_page1.html"; +const FRAME_MANUAL_PAGE2 = BASE + "/file_webNavigation_manualSubframe_page2.html"; +const INVALID_PAGE = "https://invalid.localhost/"; + +const REQUIRED = [ + "onBeforeNavigate", + "onCommitted", + "onDOMContentLoaded", + "onCompleted", +]; + +var received = []; +var completedResolve; +var waitingURL, waitingEvent; + +function loadAndWait(win, event, url, script) { + received = []; + waitingEvent = event; + waitingURL = url; + dump(`RUN ${script}\n`); + script(); + return new Promise(resolve => { completedResolve = resolve; }); +} + +add_task(async function webnav_transitions_props() { + function backgroundScriptTransitions() { + const EVENTS = [ + "onCommitted", + "onHistoryStateUpdated", + "onReferenceFragmentUpdated", + "onCompleted", + ]; + + function gotEvent(event, details) { + browser.test.log(`Got ${event} ${details.url} ${details.transitionType} ${details.transitionQualifiers && JSON.stringify(details.transitionQualifiers)}`); + + browser.test.sendMessage("received", {url: details.url, details, event}); + } + + let listeners = {}; + for (let event of EVENTS) { + listeners[event] = gotEvent.bind(null, event); + browser.webNavigation[event].addListener(listeners[event]); + } + + browser.test.sendMessage("ready"); + } + + let extensionData = { + manifest: { + permissions: [ + "webNavigation", + ], + }, + background: backgroundScriptTransitions, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + extension.onMessage("received", ({url, event, details}) => { + received.push({url, event, details}); + + if (event == waitingEvent && url == waitingURL) { + completedResolve(); + } + }); + + await Promise.all([extension.startup(), extension.awaitMessage("ready")]); + info("webnavigation extension loaded"); + + let win = window.open(); + + await loadAndWait(win, "onCompleted", URL, () => { win.location = URL; }); + + // transitionType: reload + received = []; + await loadAndWait(win, "onCompleted", URL, () => { win.location.reload(); }); + + let found = received.find((data) => (data.event == "onCommitted" && data.url == URL)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "reload", + "Got the expected 'reload' transitionType in the OnCommitted event"); + ok(Array.isArray(found.details.transitionQualifiers), + "transitionQualifiers found in the OnCommitted events"); + } + + // transitionType: auto_subframe + found = received.find((data) => (data.event == "onCommitted" && data.url == FRAME)); + + ok(found, "Got the sub-frame onCommitted event"); + + if (found) { + is(found.details.transitionType, "auto_subframe", + "Got the expected 'auto_subframe' transitionType in the OnCommitted event"); + ok(Array.isArray(found.details.transitionQualifiers), + "transitionQualifiers found in the OnCommitted events"); + } + + // transitionType: form_submit + received = []; + await loadAndWait(win, "onCompleted", FORM_URL, () => { + win.document.querySelector("form").submit(); + }); + + found = received.find((data) => (data.event == "onCommitted" && data.url == FORM_URL)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "form_submit", + "Got the expected 'form_submit' transitionType in the OnCommitted event"); + ok(Array.isArray(found.details.transitionQualifiers), + "transitionQualifiers found in the OnCommitted events"); + } + + // transitionQualifier: server_redirect + received = []; + await loadAndWait(win, "onCompleted", REDIRECTED, () => { win.location = REDIRECT; }); + + found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "link", + "Got the expected 'link' transitionType in the OnCommitted event"); + ok(Array.isArray(found.details.transitionQualifiers) && + found.details.transitionQualifiers.find((q) => q == "server_redirect"), + "Got the expected 'server_redirect' transitionQualifiers in the OnCommitted events"); + } + + // transitionQualifier: forward_back + received = []; + await loadAndWait(win, "onCompleted", FORM_URL, () => { win.history.back(); }); + + found = received.find((data) => (data.event == "onCommitted" && data.url == FORM_URL)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "link", + "Got the expected 'link' transitionType in the OnCommitted event"); + ok(Array.isArray(found.details.transitionQualifiers) && + found.details.transitionQualifiers.find((q) => q == "forward_back"), + "Got the expected 'forward_back' transitionQualifiers in the OnCommitted events"); + } + + // transitionQualifier: client_redirect + // (from meta http-equiv tag) + received = []; + await loadAndWait(win, "onCompleted", REDIRECTED, () => { + win.location = CLIENT_REDIRECT; + }); + + found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "link", + "Got the expected 'link' transitionType in the OnCommitted event"); + ok(Array.isArray(found.details.transitionQualifiers) && + found.details.transitionQualifiers.find((q) => q == "client_redirect"), + "Got the expected 'client_redirect' transitionQualifiers in the OnCommitted events"); + } + + // transitionQualifier: client_redirect + // (from http headers) + received = []; + await loadAndWait(win, "onCompleted", REDIRECTED, () => { + win.location = CLIENT_REDIRECT_HTTPHEADER; + }); + + found = received.find((data) => (data.event == "onCommitted" && + data.url == CLIENT_REDIRECT_HTTPHEADER)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "link", + "Got the expected 'link' transitionType in the OnCommitted event"); + ok(Array.isArray(found.details.transitionQualifiers) && + found.details.transitionQualifiers.find((q) => q == "client_redirect"), + "Got the expected 'client_redirect' transitionQualifiers in the OnCommitted events"); + } + + // transitionQualifier: client_redirect (sub-frame) + // (from meta http-equiv tag) + received = []; + await loadAndWait(win, "onCompleted", REDIRECTED, () => { + win.location = FRAME_CLIENT_REDIRECT; + }); + + found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "auto_subframe", + "Got the expected 'auto_subframe' transitionType in the OnCommitted event"); + ok(Array.isArray(found.details.transitionQualifiers) && + found.details.transitionQualifiers.find((q) => q == "client_redirect"), + "Got the expected 'client_redirect' transitionQualifiers in the OnCommitted events"); + } + + // transitionQualifier: server_redirect (sub-frame) + received = []; + await loadAndWait(win, "onCompleted", REDIRECTED, () => { win.location = FRAME_REDIRECT; }); + + found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECT)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "auto_subframe", + "Got the expected 'auto_subframe' transitionType in the OnCommitted event"); + // TODO BUG 1264936: currently the server_redirect is not detected in sub-frames + // once we fix it we can test it here: + // + // ok(Array.isArray(found.details.transitionQualifiers) && + // found.details.transitionQualifiers.find((q) => q == "server_redirect"), + // "Got the expected 'server_redirect' transitionQualifiers in the OnCommitted events"); + } + + // transitionType: manual_subframe + received = []; + await loadAndWait(win, "onCompleted", FRAME_MANUAL, () => { win.location = FRAME_MANUAL; }); + found = received.find((data) => (data.event == "onCommitted" && + data.url == FRAME_MANUAL_PAGE1)); + + ok(found, "Got the onCommitted event"); + + if (found) { + is(found.details.transitionType, "auto_subframe", + "Got the expected 'auto_subframe' transitionType in the OnCommitted event"); + } + + received = []; + await loadAndWait(win, "onCompleted", FRAME_MANUAL_PAGE2, () => { + let el = win.document.querySelector("iframe") + .contentDocument.querySelector("a"); + sendMouseEvent({type: "click"}, el, win); + }); + + found = received.find((data) => (data.event == "onCommitted" && + data.url == FRAME_MANUAL_PAGE2)); + + ok(found, "Got the onCommitted event"); + + if (found) { + if (AppConstants.MOZ_BUILD_APP === "browser") { + is(found.details.transitionType, "manual_subframe", + "Got the expected 'manual_subframe' transitionType in the OnCommitted event"); + } else { + is(found.details.transitionType, "auto_subframe", + "Got the expected 'manual_subframe' transitionType in the OnCommitted event"); + } + } + + // Test transitions properties on onHistoryStateUpdated events. + + received = []; + await loadAndWait(win, "onCompleted", FRAME2, () => { win.location = FRAME2; }); + + received = []; + await loadAndWait(win, "onHistoryStateUpdated", `${FRAME2}/pushState`, () => { + win.history.pushState({}, "History PushState", `${FRAME2}/pushState`); + }); + + found = received.find((data) => (data.event == "onHistoryStateUpdated" && + data.url == `${FRAME2}/pushState`)); + + ok(found, "Got the onHistoryStateUpdated event"); + + if (found) { + is(typeof found.details.transitionType, "string", + "Got transitionType in the onHistoryStateUpdated event"); + ok(Array.isArray(found.details.transitionQualifiers), + "Got transitionQualifiers in the onHistoryStateUpdated event"); + } + + // Test transitions properties on onReferenceFragmentUpdated events. + + received = []; + await loadAndWait(win, "onReferenceFragmentUpdated", `${FRAME2}/pushState#ref2`, () => { + win.history.pushState({}, "ReferenceFragment Update", `${FRAME2}/pushState#ref2`); + }); + + found = received.find((data) => (data.event == "onReferenceFragmentUpdated" && + data.url == `${FRAME2}/pushState#ref2`)); + + ok(found, "Got the onReferenceFragmentUpdated event"); + + if (found) { + is(typeof found.details.transitionType, "string", + "Got transitionType in the onReferenceFragmentUpdated event"); + ok(Array.isArray(found.details.transitionQualifiers), + "Got transitionQualifiers in the onReferenceFragmentUpdated event"); + } + + // cleanup phase + win.close(); + + await extension.unload(); + info("webnavigation extension unloaded"); +}); + +add_task(async function webnav_ordering() { + let extensionData = { + manifest: { + permissions: [ + "webNavigation", + ], + }, + background: backgroundScript, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + extension.onMessage("received", ({url, event}) => { + received.push({url, event}); + + if (event == waitingEvent && url == waitingURL) { + completedResolve(); + } + }); + + await extension.startup(); + await extension.awaitMessage("ready"); + info("webnavigation extension loaded"); + + let win = window.open(); + + await loadAndWait(win, "onCompleted", URL, () => { win.location = URL; }); + + function checkRequired(url) { + for (let event of REQUIRED) { + let found = false; + for (let r of received) { + if (r.url == url && r.event == event) { + found = true; + } + } + ok(found, `Received event ${event} from ${url}`); + } + } + + checkRequired(URL); + checkRequired(FRAME); + + function checkBefore(action1, action2) { + function find(action) { + for (let i = 0; i < received.length; i++) { + if (received[i].url == action.url && received[i].event == action.event) { + return i; + } + } + return -1; + } + + let index1 = find(action1); + let index2 = find(action2); + ok(index1 != -1, `Action ${JSON.stringify(action1)} happened`); + ok(index2 != -1, `Action ${JSON.stringify(action2)} happened`); + ok(index1 < index2, `Action ${JSON.stringify(action1)} happened before ${JSON.stringify(action2)}`); + } + + // As required in the webNavigation API documentation: + // If a navigating frame contains subframes, its onCommitted is fired before any + // of its children's onBeforeNavigate; while onCompleted is fired after + // all of its children's onCompleted. + checkBefore({url: URL, event: "onCommitted"}, {url: FRAME, event: "onBeforeNavigate"}); + checkBefore({url: FRAME, event: "onCompleted"}, {url: URL, event: "onCompleted"}); + + // As required in the webNAvigation API documentation, check the event sequence: + // onBeforeNavigate -> onCommitted -> onDOMContentLoaded -> onCompleted + let expectedEventSequence = [ + "onBeforeNavigate", "onCommitted", "onDOMContentLoaded", "onCompleted", + ]; + + for (let i = 1; i < expectedEventSequence.length; i++) { + let after = expectedEventSequence[i]; + let before = expectedEventSequence[i - 1]; + checkBefore({url: URL, event: before}, {url: URL, event: after}); + checkBefore({url: FRAME, event: before}, {url: FRAME, event: after}); + } + + await loadAndWait(win, "onCompleted", FRAME2, () => { win.frames[0].location = FRAME2; }); + + checkRequired(FRAME2); + + let navigationSequence = [ + { + action: () => { win.frames[0].document.getElementById("elt").click(); }, + waitURL: `${FRAME2}#ref`, + expectedEvent: "onReferenceFragmentUpdated", + description: "clicked an anchor link", + }, + { + action: () => { win.frames[0].history.pushState({}, "History PushState", `${FRAME2}#ref2`); }, + waitURL: `${FRAME2}#ref2`, + expectedEvent: "onReferenceFragmentUpdated", + description: "history.pushState, same pathname, different hash", + }, + { + action: () => { win.frames[0].history.pushState({}, "History PushState", `${FRAME2}#ref2`); }, + waitURL: `${FRAME2}#ref2`, + expectedEvent: "onHistoryStateUpdated", + description: "history.pushState, same pathname, same hash", + }, + { + action: () => { + win.frames[0].history.pushState({}, "History PushState", `${FRAME2}?query_param1=value#ref2`); + }, + waitURL: `${FRAME2}?query_param1=value#ref2`, + expectedEvent: "onHistoryStateUpdated", + description: "history.pushState, same pathname, same hash, different query params", + }, + { + action: () => { + win.frames[0].history.pushState({}, "History PushState", `${FRAME2}?query_param2=value#ref3`); + }, + waitURL: `${FRAME2}?query_param2=value#ref3`, + expectedEvent: "onHistoryStateUpdated", + description: "history.pushState, same pathname, different hash, different query params", + }, + { + action: () => { win.frames[0].history.pushState(null, "History PushState", FRAME_PUSHSTATE); }, + waitURL: FRAME_PUSHSTATE, + expectedEvent: "onHistoryStateUpdated", + description: "history.pushState, different pathname", + }, + ]; + + for (let navigation of navigationSequence) { + let {expectedEvent, waitURL, action, description} = navigation; + info(`Waiting ${expectedEvent} from ${waitURL} - ${description}`); + await loadAndWait(win, expectedEvent, waitURL, action); + info(`Received ${expectedEvent} from ${waitURL} - ${description}`); + } + + for (let i = navigationSequence.length - 1; i > 0; i--) { + let {waitURL: fromURL, expectedEvent} = navigationSequence[i]; + let {waitURL} = navigationSequence[i - 1]; + info(`Waiting ${expectedEvent} from ${waitURL} - history.back() from ${fromURL} to ${waitURL}`); + await loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.back(); }); + info(`Received ${expectedEvent} from ${waitURL} - history.back() from ${fromURL} to ${waitURL}`); + } + + for (let i = 0; i < navigationSequence.length - 1; i++) { + let {waitURL: fromURL} = navigationSequence[i]; + let {waitURL, expectedEvent} = navigationSequence[i + 1]; + info(`Waiting ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`); + await loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.forward(); }); + info(`Received ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`); + } + + win.close(); + + await extension.unload(); + info("webnavigation extension unloaded"); +}); + +add_task(async function webnav_error_event() { + function backgroundScriptErrorEvent() { + browser.webNavigation.onErrorOccurred.addListener((details) => { + browser.test.log(`Got onErrorOccurred ${details.url} ${details.error}`); + + browser.test.sendMessage("received", {url: details.url, details, event: "onErrorOccurred"}); + }); + + browser.test.sendMessage("ready"); + } + + let extensionData = { + manifest: { + permissions: [ + "webNavigation", + ], + }, + background: backgroundScriptErrorEvent, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + extension.onMessage("received", ({url, event, details}) => { + received.push({url, event, details}); + + if (event == waitingEvent && url == waitingURL) { + completedResolve(); + } + }); + + await Promise.all([extension.startup(), extension.awaitMessage("ready")]); + info("webnavigation extension loaded"); + + let win = window.open(); + + received = []; + await loadAndWait(win, "onErrorOccurred", INVALID_PAGE, () => { win.location = INVALID_PAGE; }); + + let found = received.find((data) => (data.event == "onErrorOccurred" && + data.url == INVALID_PAGE)); + + ok(found, "Got the onErrorOccurred event"); + + if (found) { + ok(found.details.error.match(/Error code [0-9]+/), + "Got the expected error string in the onErrorOccurred event"); + } + + // cleanup phase + win.close(); + + await extension.unload(); + info("webnavigation extension unloaded"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html new file mode 100644 index 0000000000..60720e9663 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html @@ -0,0 +1,299 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_webnav_unresolved_uri_on_expected_URI_scheme() { + function background() { + let listeners = []; + + function cleanupTestListeners() { + browser.test.log(`Cleanup previous test event listeners`); + for (let {event, listener} of listeners.splice(0)) { + browser.webNavigation[event].removeListener(listener); + } + } + + function createTestListener(event, fail, urlFilter) { + return new Promise(resolve => { + function listener(details) { + let log = JSON.stringify({url: details.url, urlFilter}); + if (fail) { + browser.test.fail(`Got an unexpected ${event} on the failure listener: ${log}`); + } else { + browser.test.succeed(`Got the expected ${event} on the success listener: ${log}`); + } + + resolve(); + } + + browser.webNavigation[event].addListener(listener, {url: urlFilter}); + listeners.push({event, listener}); + }); + } + + browser.test.onMessage.addListener((msg, events, data) => { + if (msg !== "test-filters") { + return; + } + + let promises = []; + + for (let {okFilter, failFilter} of data.filters) { + for (let event of events) { + promises.push( + Promise.race([ + createTestListener(event, false, okFilter), + createTestListener(event, true, failFilter), + ])); + } + } + + Promise.all(promises).catch(e => { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + }).then(() => { + cleanupTestListeners(); + browser.test.sendMessage("test-filter-next"); + }); + + browser.test.sendMessage("test-filter-ready"); + }); + } + + let extensionData = { + manifest: { + permissions: [ + "webNavigation", + ], + }, + background, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + await extension.startup(); + + let win = window.open(); + + let testFilterScenarios = [ + { + url: "http://example.net/browser", + filters: [ + // schemes + { + okFilter: [{schemes: ["http"]}], + failFilter: [{schemes: ["https"]}], + }, + // ports + { + okFilter: [{ports: [80, 22, 443]}], + failFilter: [{ports: [81, 82, 83]}], + }, + { + okFilter: [{ports: [22, 443, [10, 80]]}], + failFilter: [{ports: [22, 23, [81, 100]]}], + }, + // multiple criteria in a single filter: + // if one of the criteria is not verified, the event should not be received. + { + okFilter: [{schemes: ["http"], ports: [80, 22, 443]}], + failFilter: [{schemes: ["http"], ports: [81, 82, 83]}], + }, + // multiple urlFilters on the same listener + // if at least one of the criteria is verified, the event should be received. + { + okFilter: [{schemes: ["https"]}, {ports: [80, 22, 443]}], + failFilter: [{schemes: ["https"]}, {ports: [81, 82, 83]}], + }, + ], + }, + { + url: "http://example.net/browser?param=1#ref", + filters: [ + // host: Equals, Contains, Prefix, Suffix + { + okFilter: [{hostEquals: "example.net"}], + failFilter: [{hostEquals: "example.com"}], + }, + { + okFilter: [{hostContains: ".example"}], + failFilter: [{hostContains: ".www"}], + }, + { + okFilter: [{hostPrefix: "example"}], + failFilter: [{hostPrefix: "www"}], + }, + { + okFilter: [{hostSuffix: "net"}], + failFilter: [{hostSuffix: "com"}], + }, + // path: Equals, Contains, Prefix, Suffix + { + okFilter: [{pathEquals: "/browser"}], + failFilter: [{pathEquals: "/"}], + }, + { + okFilter: [{pathContains: "brow"}], + failFilter: [{pathContains: "tool"}], + }, + { + okFilter: [{pathPrefix: "/bro"}], + failFilter: [{pathPrefix: "/tool"}], + }, + { + okFilter: [{pathSuffix: "wser"}], + failFilter: [{pathSuffix: "kit"}], + }, + // query: Equals, Contains, Prefix, Suffix + { + okFilter: [{queryEquals: "param=1"}], + failFilter: [{queryEquals: "wrongparam=2"}], + }, + { + okFilter: [{queryContains: "param"}], + failFilter: [{queryContains: "wrongparam"}], + }, + { + okFilter: [{queryPrefix: "param="}], + failFilter: [{queryPrefix: "wrong"}], + }, + { + okFilter: [{querySuffix: "=1"}], + failFilter: [{querySuffix: "=2"}], + }, + // urlMatches, originAndPathMatches + { + okFilter: [{urlMatches: "example.net/.*\?param=1"}], + failFilter: [{urlMatches: "example.net/.*\?wrongparam=2"}], + }, + { + okFilter: [{originAndPathMatches: "example.net\/browser"}], + failFilter: [{originAndPathMatches: "example.net/.*\?param=1"}], + }, + ], + }, + ]; + + info("WebNavigation event filters test scenarios starting..."); + + const EVENTS = [ + "onBeforeNavigate", + "onCommitted", + "onDOMContentLoaded", + "onCompleted", + ]; + + for (let data of testFilterScenarios) { + info(`Prepare the new test scenario: ${JSON.stringify(data)}`); + + // Bug 1589102: using plain "about:blank" crashes here in fission+debug. + win.location = "about:blank?2"; + + extension.sendMessage("test-filters", EVENTS, data); + await extension.awaitMessage("test-filter-ready"); + + info(`Loading the test url: ${data.url}`); + win.location = data.url; + + await extension.awaitMessage("test-filter-next"); + + info("Test scenario completed. Moving to the next test scenario."); + } + + info("WebNavigation event filters test onReferenceFragmentUpdated scenario starting..."); + + const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; + let url = BASE + "/file_WebNavigation_page3.html"; + + let okFilter = [{urlContains: "_page3.html"}]; + let failFilter = [{ports: [444]}]; + let data = {filters: [{okFilter, failFilter}]}; + let event = "onCompleted"; + + info(`Loading the initial test url: ${url}`); + extension.sendMessage("test-filters", [event], data); + + await extension.awaitMessage("test-filter-ready"); + win.location = url; + await extension.awaitMessage("test-filter-next"); + + event = "onReferenceFragmentUpdated"; + extension.sendMessage("test-filters", [event], data); + + await extension.awaitMessage("test-filter-ready"); + win.location = url + "#ref1"; + await extension.awaitMessage("test-filter-next"); + + info("WebNavigation event filters test onHistoryStateUpdated scenario starting..."); + + event = "onHistoryStateUpdated"; + extension.sendMessage("test-filters", [event], data); + await extension.awaitMessage("test-filter-ready"); + + win.history.pushState({}, "", BASE + "/pushState_page3.html"); + await extension.awaitMessage("test-filter-next"); + + // TODO: add additional specific tests for the other webNavigation events: + // onErrorOccurred (and onCreatedNavigationTarget on supported) + + info("WebNavigation event filters test scenarios completed."); + + await extension.unload(); + + win.close(); +}); + +add_task(async function test_webnav_empty_filter_validation_error() { + function background() { + let catchedException; + + try { + browser.webNavigation.onCompleted.addListener( + // Empty callback (not really used) + () => {}, + // Empty filter (which should raise a validation error exception). + {url: []} + ); + } catch (e) { + catchedException = e; + browser.test.log(`Got an exception`); + } + + if (catchedException && + catchedException.message.includes("Type error for parameter filters") && + catchedException.message.includes("Array requires at least 1 items; you have 0")) { + browser.test.notifyPass("webNav.emptyFilterValidationError"); + } else { + browser.test.notifyFail("webNav.emptyFilterValidationError"); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webNavigation", + ], + }, + background, + }); + + await extension.startup(); + + await extension.awaitFinish("webNav.emptyFilterValidationError"); + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_incognito.html b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_incognito.html new file mode 100644 index 0000000000..6b161d8247 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_incognito.html @@ -0,0 +1,109 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function webnav_test_incognito() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.allowPrivateBrowsingByDefault", false]], + }); + + // Monitor will fail if it gets any event. + let monitor = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["webNavigation", "*://mochi.test/*"], + }, + background() { + const EVENTS = [ + "onTabReplaced", + "onBeforeNavigate", + "onCommitted", + "onDOMContentLoaded", + "onCompleted", + "onErrorOccurred", + "onReferenceFragmentUpdated", + "onHistoryStateUpdated", + ]; + + function onEvent(event, details) { + browser.test.fail(`not_allowed - Got ${event} ${details.url} ${details.frameId} ${details.parentFrameId}`); + } + + let listeners = {}; + for (let event of EVENTS) { + listeners[event] = onEvent.bind(null, event); + browser.webNavigation[event].addListener(listeners[event]); + } + + browser.test.onMessage.addListener(async (message, tabId) => { + // try to access the private window + await browser.test.assertRejects(browser.webNavigation.getAllFrames({tabId}), + /Invalid tab ID/, + "should not be able to get incognito frames"); + await browser.test.assertRejects(browser.webNavigation.getFrame({tabId, frameId: 0}), + /Invalid tab ID/, + "should not be able to get incognito frames"); + browser.test.notifyPass("completed"); + }); + }, + }); + + // extension loads a private window and waits for the onCompleted event. + let extension = ExtensionTestUtils.loadExtension({ + incognitoOverride: "spanning", + manifest: { + permissions: ["tabs", "webNavigation", "*://mochi.test/*"], + }, + async background() { + const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; + const url = BASE + "/file_WebNavigation_page1.html"; + let window; + + browser.webNavigation.onCompleted.addListener(async (details) => { + if (details.url !== url) { + return; + } + browser.test.log(`spanning - Got onCompleted ${details.url} ${details.frameId} ${details.parentFrameId}`); + browser.test.sendMessage("completed"); + }); + browser.test.onMessage.addListener(async () => { + await browser.windows.remove(window.id); + browser.test.notifyPass("done"); + }); + window = await browser.windows.create({url, incognito: true}); + let tabs = await browser.tabs.query({active: true, windowId: window.id}); + browser.test.sendMessage("tabId", tabs[0].id); + }, + }); + + await monitor.startup(); + await extension.startup(); + + await extension.awaitMessage("completed"); + let tabId = await extension.awaitMessage("tabId"); + + await monitor.sendMessage("tab", tabId); + await monitor.awaitFinish("completed"); + + await extension.sendMessage("close"); + await extension.awaitFinish("done"); + + await extension.unload(); + await monitor.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_and_proxy_filter.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_and_proxy_filter.html new file mode 100644 index 0000000000..ea99ec244a --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_and_proxy_filter.html @@ -0,0 +1,134 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +<script> +"use strict"; + +// Check that the windowId and tabId filter work as expected in the webRequest +// and proxy API: +// - A non-matching windowId / tabId listener won't trigger events. +// - A matching tabId from a tab triggers the event. +// - A matching windowId from a tab triggers the event. +// (unlike test_ext_webrequest_filter.html, this also works on Android) +// - Requests from background pages can be matched with windowId and tabId -1. +add_task(async function test_filter_tabId_and_windowId() { + async function tabScript() { + let pendingExpectations = new Set(); + // Helper to detect completion of expected requests. + function watchExpected(filter, desc) { + desc += ` - ${JSON.stringify(filter)}`; + const DESC_PROXY = `${desc} (proxy)`; + const DESC_WEBREQUEST = `${desc} (webRequest)`; + pendingExpectations.add(DESC_PROXY); + pendingExpectations.add(DESC_WEBREQUEST); + browser.proxy.onRequest.addListener(() => { + pendingExpectations.delete(DESC_PROXY); + }, filter); + browser.webRequest.onBeforeRequest.addListener( + () => { + pendingExpectations.delete(DESC_WEBREQUEST); + }, + filter, + ["blocking"] + ); + } + + // Helper to detect unexpected requests. + function watchUnexpected(filter, desc) { + desc += ` - ${JSON.stringify(filter)}`; + browser.proxy.onRequest.addListener(() => { + browser.test.fail(`${desc} - unexpected proxy event`); + }, filter); + browser.webRequest.onBeforeRequest.addListener(() => { + browser.test.fail(`${desc} - unexpected webRequest event`); + }, filter); + } + + function registerExpectations(url, windowId, tabId) { + const urls = [url]; + watchUnexpected({ urls, windowId: 0 }, "non-matching windowId"); + watchUnexpected({ urls, tabId: 0 }, "non-matching tabId"); + + watchExpected({ urls, windowId }, "windowId matches"); + watchExpected({ urls, tabId }, "tabId matches"); + } + + try { + let { windowId, tabId } = await browser.runtime.sendMessage("getIds"); + browser.test.log(`Dummy tab has: tabId=${tabId} windowId=${windowId}`); + registerExpectations("http://example.com/?tab", windowId, tabId); + registerExpectations("http://example.com/?bg", -1, -1); + + // Call an API method implemented in the parent process to ensure that + // the listeners have been registered (workaround for bug 1300234). + // There is a .catch() at the end because the call is rejected on Android. + await browser.proxy.settings.get({}).catch(() => {}); + + browser.test.log("Triggering request from background page."); + await browser.runtime.sendMessage("triggerBackgroundRequest"); + + browser.test.log("Triggering request from tab."); + await fetch("http://example.com/?tab"); + + browser.test.assertEq(0, pendingExpectations.size, "got all events"); + for (let description of pendingExpectations) { + browser.test.fail(`Event not observed: ${description}`); + } + } catch (e) { + browser.test.fail(`Unexpected test failure: ${e} :: ${e.stack}`); + } + browser.test.sendMessage("testCompleted"); + } + + function background() { + browser.runtime.onMessage.addListener(async (msg, sender) => { + if (msg === "getIds") { + return { windowId: sender.tab.windowId, tabId: sender.tab.id }; + } + if (msg === "triggerBackgroundRequest") { + await fetch("http://example.com/?bg"); + } + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "proxy", + "webRequest", + "webRequestBlocking", + "http://example.com/*", + ], + web_accessible_resources: ["tab.html"], + }, + background, + files: { + "tab.html": `<!DOCTYPE html><script src="tab.js"><\/script>`, + "tab.js": tabScript, + }, + }); + await extension.startup(); + + // bug 1641735: tabs.create / tabs.remove does not work in GeckoView unless + // `useAddonManager: "permanent"` is used, so use window.open() instead. + // + // Note that somehow window.open() unexpectedly runs null when extensions + // run in-process, i.e. extensions.webextensions.remote=false. Fortunately, + // extension tabs are automatically closed as part of extension.unload() + // below (provided that extension APIs are used in the tab - bug 1399655). + window.open(`moz-extension://${extension.uuid}/tab.html`); + + await extension.awaitMessage("testCompleted"); + await extension.unload(); +}); +</script> +</head> +<body> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html new file mode 100644 index 0000000000..f191e58b56 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html @@ -0,0 +1,182 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head_webrequest.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +// This file defines content scripts. +/* eslint-env mozilla/frame-script */ + +let baseUrl = "http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/authenticate.sjs"; +function testXHR(url) { + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + xhr.open("GET", url); + xhr.onload = resolve; + xhr.onabort = reject; + xhr.onerror = reject; + xhr.send(); + }); +} + +function getAuthHandler(result, blocking = true) { + function background(result) { + browser.webRequest.onAuthRequired.addListener((details) => { + browser.test.succeed(`authHandler.onAuthRequired called with ${details.requestId} ${details.url} result ${JSON.stringify(result)}`); + browser.test.sendMessage("onAuthRequired"); + return result; + }, {urls: ["*://mochi.test/*"]}, ["blocking"]); + browser.webRequest.onCompleted.addListener((details) => { + browser.test.succeed(`authHandler.onCompleted called with ${details.requestId} ${details.url}`); + browser.test.sendMessage("onCompleted"); + }, {urls: ["*://mochi.test/*"]}); + browser.webRequest.onErrorOccurred.addListener((details) => { + browser.test.succeed(`authHandler.onErrorOccurred called with ${details.requestId} ${details.url}`); + browser.test.sendMessage("onErrorOccurred"); + }, {urls: ["*://mochi.test/*"]}); + } + + let permissions = [ + "webRequest", + "*://mochi.test/*", + ]; + if (blocking) { + permissions.push("webRequestBlocking"); + } + return ExtensionTestUtils.loadExtension({ + manifest: { + permissions, + }, + background: `(${background})(${JSON.stringify(result)})`, + }); +} + +add_task(async function test_webRequest_auth_nonblocking_forwardAuthProvider() { + // The chrome script sets up a default auth handler on the channel, the + // extension does not return anything in the authRequred call. We should + // get the call in the extension first, then in the chrome code where we + // cancel the request to avoid dealing with the prompt dialog here. The test + // is to ensure that WebRequest calls the previous notificationCallbacks + // if the authorization is not handled by the onAuthRequired handler. + + let chromeScript = SpecialPowers.loadChromeScript(() => { + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + + let observer = channel => { + if (!(channel instanceof Ci.nsIHttpChannel && channel.URI.host === "mochi.test")) { + return; + } + Services.obs.removeObserver(observer, "http-on-modify-request"); + channel.notificationCallbacks = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor", + "nsIAuthPromptProvider", + "nsIAuthPrompt2"]), + getInterface: ChromeUtils.generateQI(["nsIAuthPromptProvider", + "nsIAuthPrompt2"]), + promptAuth(channel, level, authInfo) { + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + getAuthPrompt(reason, iid) { + return this; + }, + asyncPromptAuth(channel, callback, context, level, authInfo) { + // We just cancel here, we're only ensuring that non-webrequest + // notificationcallbacks get called if webrequest doesn't handle it. + Promise.resolve().then(() => { + callback.onAuthCancelled(context, false); + channel.cancel(Cr.NS_BINDING_ABORTED); + sendAsyncMessage("callback-complete"); + }); + }, + }; + }; + Services.obs.addObserver(observer, "http-on-modify-request"); + sendAsyncMessage("chrome-ready"); + }); + await chromeScript.promiseOneMessage("chrome-ready"); + let callbackComplete = chromeScript.promiseOneMessage("callback-complete"); + + let handlingExt = getAuthHandler(); + await handlingExt.startup(); + + await Assert.rejects(testXHR(`${baseUrl}?realm=auth_nonblocking_forwardAuth&user=auth_nonblocking_forwardAuth&pass=auth_nonblocking_forwardAuth`), + ProgressEvent, + "caught rejected xhr"); + + await callbackComplete; + await handlingExt.awaitMessage("onAuthRequired"); + // We expect onErrorOccurred because the "default" authprompt above cancelled + // the auth request to avoid a dialog. + await handlingExt.awaitMessage("onErrorOccurred"); + await handlingExt.unload(); + chromeScript.destroy(); +}); + +add_task(async function test_webRequest_auth_nonblocking_forwardAuthPrompt2() { + // The chrome script sets up a default auth handler on the channel, the + // extension does not return anything in the authRequred call. We should + // get the call in the extension first, then in the chrome code where we + // cancel the request to avoid dealing with the prompt dialog here. The test + // is to ensure that WebRequest calls the previous notificationCallbacks + // if the authorization is not handled by the onAuthRequired handler. + + let chromeScript = SpecialPowers.loadChromeScript(() => { + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + + let observer = channel => { + if (!(channel instanceof Ci.nsIHttpChannel && channel.URI.host === "mochi.test")) { + return; + } + Services.obs.removeObserver(observer, "http-on-modify-request"); + channel.notificationCallbacks = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor", + "nsIAuthPrompt2"]), + getInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), + promptAuth(request, level, authInfo) { + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + asyncPromptAuth(request, callback, context, level, authInfo) { + // We just cancel here, we're only ensuring that non-webrequest + // notificationcallbacks get called if webrequest doesn't handle it. + Promise.resolve().then(() => { + request.cancel(Cr.NS_BINDING_ABORTED); + sendAsyncMessage("callback-complete"); + }); + }, + }; + }; + Services.obs.addObserver(observer, "http-on-modify-request"); + sendAsyncMessage("chrome-ready"); + }); + await chromeScript.promiseOneMessage("chrome-ready"); + let callbackComplete = chromeScript.promiseOneMessage("callback-complete"); + + let handlingExt = getAuthHandler(); + await handlingExt.startup(); + + await Assert.rejects(testXHR(`${baseUrl}?realm=auth_nonblocking_forwardAuthPromptProvider&user=auth_nonblocking_forwardAuth&pass=auth_nonblocking_forwardAuth`), + ProgressEvent, + "caught rejected xhr"); + + await callbackComplete; + await handlingExt.awaitMessage("onAuthRequired"); + // We expect onErrorOccurred because the "default" authprompt above cancelled + // the auth request to avoid a dialog. + await handlingExt.awaitMessage("onErrorOccurred"); + await handlingExt.unload(); + chromeScript.destroy(); +}); +</script> +</head> +<body> +<div id="test">Authorization Test</div> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html new file mode 100644 index 0000000000..86cec62fb4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html @@ -0,0 +1,120 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_webRequest_serviceworker_events() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.serviceWorkers.testing.enabled", true]], + }); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "<all_urls>", + ], + }, + background() { + let eventNames = new Set([ + "onBeforeRequest", + "onBeforeSendHeaders", + "onSendHeaders", + "onHeadersReceived", + "onResponseStarted", + "onCompleted", + "onErrorOccurred", + ]); + + function listener(name, details) { + browser.test.assertTrue(eventNames.has(name), `received ${name}`); + eventNames.delete(name); + if (name == "onCompleted") { + eventNames.delete("onErrorOccurred"); + } else if (name == "onErrorOccurred") { + eventNames.delete("onCompleted"); + } + if (eventNames.size == 0) { + browser.test.sendMessage("done"); + } + } + + for (let name of eventNames) { + browser.webRequest[name].addListener( + listener.bind(null, name), + {urls: ["https://example.com/*"]} + ); + } + }, + }); + + await extension.startup(); + let registration = await navigator.serviceWorker.register("webrequest_worker.js", {scope: "."}); + await waitForState(registration.installing, "activated"); + await extension.awaitMessage("done"); + await registration.unregister(); + await extension.unload(); +}); + +add_task(async function test_webRequest_background_events() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "<all_urls>", + ], + }, + background() { + let eventNames = new Set([ + "onBeforeRequest", + "onBeforeSendHeaders", + "onSendHeaders", + "onHeadersReceived", + "onResponseStarted", + "onCompleted", + ]); + + function listener(name, details) { + browser.test.assertTrue(eventNames.has(name), `received ${name}`); + eventNames.delete(name); + + if (eventNames.size === 0) { + browser.test.assertEq("xmlhttprequest", details.type, "correct type for fetch [see bug 1366710]"); + browser.test.assertEq(0, eventNames.size, "messages received"); + browser.test.sendMessage("done"); + } + } + + for (let name of eventNames) { + browser.webRequest[name].addListener( + listener.bind(null, name), + {urls: ["https://example.com/*"]} + ); + } + + fetch("https://example.com/example.txt").then(() => { + browser.test.succeed("Fetch succeeded."); + }, () => { + browser.test.fail("fetch received"); + browser.test.sendMessage("done"); + }); + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html new file mode 100644 index 0000000000..742f048a8a --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html @@ -0,0 +1,446 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head_webrequest.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +function promiseWindowEvent(name, accept) { + return new Promise(resolve => { + window.addEventListener(name, function listener(event) { + if (event.data !== accept) { + return; + } + window.removeEventListener(name, listener); + resolve(event); + }); + }); +} + +if (AppConstants.platform === "android") { + SimpleTest.requestLongerTimeout(3); +} + +let extension; +add_task(async function setup() { + // Clear the image cache, since it gets in the way otherwise. + let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools); + let cache = imgTools.getImgCacheForDocument(document); + cache.clearCache(false); + function clearCache() { + ChromeUtils.import("resource://gre/modules/Services.jsm", {}).Services.cache2.clear(); + } + SpecialPowers.loadChromeScript(clearCache); + + await SpecialPowers.pushPrefEnv({ + set: [["network.http.rcwn.enabled", false]], + }); + + extension = makeExtension(); + await extension.startup(); +}); + +// expect is a set of test values used by the background script. +// +// type: type of request action +// events: optional, If defined only the events listed are expected for the +// request. If undefined, all events except onErrorOccurred +// and onBeforeRedirect are expected. Must be in order received. +// redirect: url to redirect to during onBeforeSendHeaders +// status: number expected status during onHeadersReceived, 200 default +// cancel: event in which we return cancel=true. cancelled message is sent. +// cached: expected fromCache value, default is false, checked in onCompletion +// headers: request or response headers to modify +// origin: The expected originUrl, a default origin can be passed for all files + +add_task(async function test_webRequest_links() { + let expect = { + "file_style_bad.css": { + type: "stylesheet", + events: ["onBeforeRequest", "onErrorOccurred"], + cancel: "onBeforeRequest", + }, + "file_style_redirect.css": { + type: "stylesheet", + events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"], + optional_events: ["onHeadersReceived"], + redirect: "file_style_good.css", + }, + "file_style_good.css": { + type: "stylesheet", + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + addStylesheet("file_style_bad.css"); + await extension.awaitMessage("cancelled"); + // we redirect to style_good which completes the test + addStylesheet("file_style_redirect.css"); + await extension.awaitMessage("done"); +}); + +add_task(async function test_webRequest_images() { + let expect = { + "file_image_bad.png": { + type: "image", + events: ["onBeforeRequest", "onErrorOccurred"], + cancel: "onBeforeRequest", + }, + "file_image_redirect.png": { + type: "image", + events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"], + optional_events: ["onHeadersReceived"], + redirect: "file_image_good.png", + }, + "file_image_good.png": { + type: "image", + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + addImage("file_image_bad.png"); + await extension.awaitMessage("cancelled"); + // we redirect to image_good which completes the test + addImage("file_image_redirect.png"); + await extension.awaitMessage("done"); +}); + +add_task(async function test_webRequest_scripts() { + let expect = { + "file_script_bad.js": { + type: "script", + events: ["onBeforeRequest", "onErrorOccurred"], + cancel: "onBeforeRequest", + }, + "file_script_redirect.js": { + type: "script", + events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"], + optional_events: ["onHeadersReceived"], + redirect: "file_script_good.js", + }, + "file_script_good.js": { + type: "script", + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + let message = promiseWindowEvent("message", "test1"); + addScript("file_script_bad.js"); + await extension.awaitMessage("cancelled"); + // we redirect to script_good which completes the test + addScript("file_script_redirect.js?q=test1"); + await extension.awaitMessage("done"); + + is((await message).data, "test1", "good script ran"); +}); + +add_task(async function test_webRequest_xhr_get() { + let expect = { + "file_script_xhr.js": { + type: "script", + }, + "xhr_resource": { + status: 404, + type: "xmlhttprequest", + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + addScript("file_script_xhr.js"); + await extension.awaitMessage("done"); +}); + +add_task(async function test_webRequest_nonexistent() { + let expect = { + "nonexistent_script_url.js": { + status: 404, + type: "script", + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + addScript("nonexistent_script_url.js"); + await extension.awaitMessage("done"); +}); + +add_task(async function test_webRequest_checkCached() { + let expect = { + "file_image_good.png": { + type: "image", + cached: true, + }, + "file_script_good.js": { + type: "script", + cached: true, + }, + "file_style_good.css": { + type: "stylesheet", + cached: true, + }, + "nonexistent_script_url.js": { + status: 404, + type: "script", + cached: false, + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + let message = promiseWindowEvent("message", "test1"); + + addImage("file_image_good.png"); + addScript("file_script_good.js?q=test1"); + + is((await message).data, "test1", "good script ran"); + + addStylesheet("file_style_good.css"); + addScript("nonexistent_script_url.js"); + await extension.awaitMessage("done"); +}); + +add_task(async function test_webRequest_headers() { + let expect = { + "file_script_nonexistent.js": { + type: "script", + status: 404, + headers: { + request: { + add: { + "X-WebRequest-request": "text", + "X-WebRequest-request-binary": "binary", + }, + modify: { + "user-agent": "WebRequest", + }, + remove: [ + "referer", + ], + }, + response: { + add: { + "X-WebRequest-response": "text", + "X-WebRequest-response-binary": "binary", + }, + modify: { + "server": "WebRequest", + "content-type": "text/html; charset=utf-8", + }, + remove: [ + "connection", + ], + }, + }, + completion: "onCompleted", + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + addScript("file_script_nonexistent.js"); + await extension.awaitMessage("done"); +}); + +add_task(async function test_webRequest_tabId() { + function background() { + let tab; + browser.tabs.onCreated.addListener(newTab => { + tab = newTab; + }); + + browser.test.onMessage.addListener(msg => { + if (msg === "close-tab") { + browser.tabs.remove(tab.id); + browser.test.sendMessage("tab-closed"); + } + }); + } + + let tabExt = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + applications: { gecko: { id: "web_request_tab_id@tests.mozilla.org" } }, + permissions: [ + "tabs", + ], + }, + background, + }); + await tabExt.startup(); + + let linkUrl = `file_WebRequest_page3.html?trigger=a&nocache=${Math.random()}`; + let expect = { + "file_WebRequest_page3.html": { + type: "main_frame", + }, + }; + + if (AppConstants.platform != "android") { + expect["favicon.ico"] = { + type: "image", + origin: SimpleTest.getTestFileURL(linkUrl), + cached: false, + }; + } + + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + let a = addLink(linkUrl); + a.click(); + await extension.awaitMessage("done"); + + let closed = tabExt.awaitMessage("tab-closed"); + tabExt.sendMessage("close-tab"); + await closed; + + await tabExt.unload(); +}); + +add_task(async function test_webRequest_tabId_browser() { + async function background(url) { + let tabId; + browser.test.onMessage.addListener(async (msg, expected) => { + if (msg == "create") { + let tab = await browser.tabs.create({url}); + tabId = tab.id; + return; + } + if (msg == "done") { + await browser.tabs.remove(tabId); + browser.test.sendMessage("done"); + } + }); + browser.test.sendMessage("origin", browser.runtime.getURL("/")); + } + + let pageUrl = `${SimpleTest.getTestFileURL("file_sample.html")}?nocache=${Math.random()}`; + let tabExt = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + applications: { gecko: { id: "tab_id_browser@tests.mozilla.org" } }, + permissions: [ + "tabs", + ], + }, + background: `(${background})('${pageUrl}')`, + }); + + let expect = { + "file_sample.html": { + type: "main_frame", + }, + }; + + if (AppConstants.platform != "android") { + expect["favicon.ico"] = { + type: "image", + origin: pageUrl, + cached: true, + }; + } + + await tabExt.startup(); + let origin = await tabExt.awaitMessage("origin"); + + // expecting origin == extension baseUrl + extension.sendMessage("set-expected", {expect, origin}); + await extension.awaitMessage("continue"); + + // open a tab from an extension principal + tabExt.sendMessage("create"); + await extension.awaitMessage("done"); + tabExt.sendMessage("done"); + await tabExt.awaitMessage("done"); + await tabExt.unload(); +}); + +add_task(async function test_webRequest_frames() { + let expect = { + "redirection.sjs": { + status: 302, + type: "sub_frame", + events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onBeforeRedirect"], + }, + "dummy_page.html": { + type: "sub_frame", + status: 404, + }, + "badrobot": { + type: "sub_frame", + status: 404, + events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onErrorOccurred"], + // When an url's hostname fails to be resolved, an NS_ERROR_NET_ON_RESOLVED/RESOLVING + // onError event may be fired right before the NS_ERROR_UNKNOWN_HOST + // (See Bug 1516862 for a rationale). + optional_events: ["onErrorOccurred"], + error: ["NS_ERROR_UNKNOWN_HOST", "NS_ERROR_NET_ON_RESOLVED", "NS_ERROR_NET_ON_RESOLVING"], + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + addFrame("redirection.sjs"); + addFrame("https://nonresolvablehostname.invalid/badrobot"); + await extension.awaitMessage("done"); +}); + +add_task(async function teardown() { + await extension.unload(); +}); + +add_task(async function test_case_preserving() { + const manifest = { + permissions: [ + "webRequest", + "webRequestBlocking", + "http://mochi.test/", + ], + }; + + async function background() { + // This is testing if header names preserve case, + // so the case-sensitive comparison is on purpose. + function ua({url, requestHeaders}) { + if (url.endsWith("?blind-add")) { + requestHeaders.push({name: "user-agent", value: "Blind/Add"}); + return {requestHeaders}; + } + for (const header of requestHeaders) { + if (header.name === "User-Agent") { + header.value = "Case/Sensitive"; + } + } + return {requestHeaders}; + } + + await browser.webRequest.onBeforeSendHeaders.addListener(ua, {urls: ["<all_urls>"]}, ["blocking", "requestHeaders"]); + browser.test.sendMessage("ready"); + } + + const extension = ExtensionTestUtils.loadExtension({manifest, background}); + + await extension.startup(); + await extension.awaitMessage("ready"); + + const response1 = await fetch(SimpleTest.getTestFileURL("return_headers.sjs")); + const headers1 = JSON.parse(await response1.text()); + + is(headers1["user-agent"], "Case/Sensitive", "User-Agent header matched and changed."); + + const response2 = await fetch(SimpleTest.getTestFileURL("return_headers.sjs?blind-add")); + const headers2 = JSON.parse(await response2.text()); + + is(headers2["user-agent"], "Blind/Add", "User-Agent header blindly added."); + + await extension.unload(); +}); + +</script> +</head> +<body> +<div id="test">Sample text</div> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_errors.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_errors.html new file mode 100644 index 0000000000..89ef9f4809 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_errors.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebRequest errors</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script type="text/javascript"> +"use strict"; + +async function test_connection_refused(url, expectedError) { + async function background(url, expectedError) { + browser.test.log(`background url is ${url}`); + browser.webRequest.onErrorOccurred.addListener(details => { + if (details.url != url) { + return; + } + browser.test.assertTrue(details.error.startsWith(expectedError), "error correct"); + browser.test.sendMessage("onErrorOccurred"); + }, {urls: ["<all_urls>"]}); + + let tabId; + browser.test.onMessage.addListener(async (msg, expected) => { + await browser.tabs.remove(tabId); + browser.test.sendMessage("done"); + }); + + let tab = await browser.tabs.create({url}); + tabId = tab.id; + } + + let extensionData = { + useAddonManager: "permanent", + manifest: { + applications: { gecko: { id: "connection_refused@tests.mozilla.org" } }, + permissions: ["webRequest", "tabs", "*://badchain.include-subdomains.pinning.example.com/*"], + }, + background: `(${background})("${url}", "${expectedError}")`, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + await extension.awaitMessage("onErrorOccurred"); + extension.sendMessage("close-tab"); + await extension.awaitMessage("done"); + + await extension.unload(); +} + +add_task(function test_bad_cert() { + return test_connection_refused("https://badchain.include-subdomains.pinning.example.com/", "Unable to communicate securely with peer"); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html new file mode 100644 index 0000000000..62539b54bc --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html @@ -0,0 +1,227 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <script type="text/javascript" src="head_webrequest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +if (AppConstants.platform === "android") { + SimpleTest.requestLongerTimeout(6); +} + +let windowData, testWindow; + +add_task(async function setup() { + let chromeScript = SpecialPowers.loadChromeScript(function() { + ChromeUtils.import("resource://gre/modules/Services.jsm", {}).Services.cache2.clear(); + }); + chromeScript.destroy(); + + testWindow = window.open("about:blank", "_blank", "width=100,height=100"); + await waitForLoad(testWindow); + + // Fetch the windowId and tabId we need to filter with WebRequest. + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "tabs", + ], + }, + background() { + browser.tabs.query({currentWindow: true}).then(tabs => { + let tab = tabs.find(tab => tab.active); + let {windowId} = tab; + + browser.test.log(`current window ${windowId} tabs: ${JSON.stringify(tabs.map(tab => [tab.id, tab.url]))}`); + browser.test.sendMessage("windowData", {windowId, tabId: tab.id}); + }); + }, + }); + await extension.startup(); + windowData = await extension.awaitMessage("windowData"); + info(`window is ${JSON.stringify(windowData)}`); + await extension.unload(); +}); + +add_task(async function test_webRequest_filter_window() { + if (AppConstants.MOZ_BUILD_APP !== "browser") { + // Android does not support multiple windows. + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [["dom.serviceWorkers.testing.enabled", true], + ["network.http.rcwn.enabled", false]], + }); + + let events = { + "onBeforeRequest": [{urls: ["<all_urls>"], windowId: windowData.windowId}], + "onBeforeSendHeaders": [{urls: ["<all_urls>"], windowId: windowData.windowId}, ["requestHeaders"]], + "onSendHeaders": [{urls: ["<all_urls>"], windowId: windowData.windowId}, ["requestHeaders"]], + "onBeforeRedirect": [{urls: ["<all_urls>"], windowId: windowData.windowId}], + "onHeadersReceived": [{urls: ["<all_urls>"], windowId: windowData.windowId}, ["responseHeaders"]], + "onResponseStarted": [{urls: ["<all_urls>"], windowId: windowData.windowId}], + "onCompleted": [{urls: ["<all_urls>"], windowId: windowData.windowId}, ["responseHeaders"]], + "onErrorOccurred": [{urls: ["<all_urls>"], windowId: windowData.windowId}], + }; + let expect = { + "file_image_bad.png": { + optional_events: ["onBeforeRedirect", "onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders"], + type: "main_frame", + }, + }; + + if (AppConstants.platform != "android") { + expect["favicon.ico"] = { + // These events only happen in non-e10s. See bug 1472156. + optional_events: ["onBeforeRedirect", "onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders"], + type: "image", + origin: SimpleTest.getTestFileURL("file_image_bad.png"), + }; + } + + let extension = makeExtension(events); + await extension.startup(); + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + + // We should not get events for a new window load. + let newWindow = window.open("file_image_good.png", "_blank", "width=100,height=100"); + await waitForLoad(newWindow); + newWindow.close(); + + // We should not get background events. + let registration = await navigator.serviceWorker.register("webrequest_worker.js?test0", {scope: "."}); + await waitForState(registration.installing, "activated"); + + // We should get events for the reload. + testWindow.location = "file_image_bad.png"; + await extension.awaitMessage("done"); + + testWindow.location = "about:blank"; + await registration.unregister(); + await extension.unload(); +}); + +add_task(async function test_webRequest_filter_tab() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.serviceWorkers.testing.enabled", true]], + }); + + let img = `file_image_good.png?r=${Math.random()}`; + + let events = { + "onBeforeRequest": [{urls: ["<all_urls>"], tabId: windowData.tabId}], + "onBeforeSendHeaders": [{urls: ["<all_urls>"], tabId: windowData.tabId}, ["requestHeaders"]], + "onSendHeaders": [{urls: ["<all_urls>"], tabId: windowData.tabId}, ["requestHeaders"]], + "onBeforeRedirect": [{urls: ["<all_urls>"], tabId: windowData.tabId}], + "onHeadersReceived": [{urls: ["<all_urls>"], tabId: windowData.tabId}, ["responseHeaders"]], + "onResponseStarted": [{urls: ["<all_urls>"], tabId: windowData.tabId}], + "onCompleted": [{urls: ["<all_urls>"], tabId: windowData.tabId}, ["responseHeaders"]], + "onErrorOccurred": [{urls: ["<all_urls>"], tabId: windowData.tabId}], + }; + let expect = { + "file_image_good.png": { + // These events only happen in non-e10s. See bug 1472156. + optional_events: ["onBeforeRedirect", "onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders"], + type: "main_frame", + // cached: AppConstants.MOZ_BUILD_APP === "browser", + }, + }; + + if (AppConstants.platform != "android") { + // A favicon request may be initiated, and complete or be aborted. + expect["favicon.ico"] = { + optional_events: ["onBeforeRedirect", "onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onResponseStarted", "onCompleted", "onErrorOccurred"], + type: "image", + origin: SimpleTest.getTestFileURL(img), + }; + } + + let extension = makeExtension(events); + await extension.startup(); + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + + if (AppConstants.MOZ_BUILD_APP === "browser") { + // We should not get events for a new window load. + let newWindow = window.open(img, "_blank", "width=100,height=100"); + await waitForLoad(newWindow); + newWindow.close(); + } + + // We should not get background events. + let registration = await navigator.serviceWorker.register("webrequest_worker.js?test1", {scope: "."}); + await waitForState(registration.installing, "activated"); + + // We should get events for the reload. + testWindow.location = img; + await extension.awaitMessage("done"); + + testWindow.location = "about:blank"; + await registration.unregister(); + await extension.unload(); +}); + + +add_task(async function test_webRequest_filter_background() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.serviceWorkers.testing.enabled", true]], + }); + + let events = { + "onBeforeRequest": [{urls: ["<all_urls>"], tabId: -1}], + "onBeforeSendHeaders": [{urls: ["<all_urls>"], tabId: -1}, ["requestHeaders"]], + "onSendHeaders": [{urls: ["<all_urls>"], tabId: -1}, ["requestHeaders"]], + "onBeforeRedirect": [{urls: ["<all_urls>"], tabId: -1}], + "onHeadersReceived": [{urls: ["<all_urls>"], tabId: -1}, ["responseHeaders"]], + "onResponseStarted": [{urls: ["<all_urls>"], tabId: -1}], + "onCompleted": [{urls: ["<all_urls>"], tabId: -1}, ["responseHeaders"]], + "onErrorOccurred": [{urls: ["<all_urls>"], tabId: -1}], + }; + let expect = { + "webrequest_worker.js": { + type: "script", + }, + "example.txt": { + status: 404, + events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onResponseStarted"], + optional_events: ["onCompleted", "onErrorOccurred"], + type: "xmlhttprequest", + origin: SimpleTest.getTestFileURL("webrequest_worker.js?test2"), + }, + }; + + let extension = makeExtension(events); + await extension.startup(); + extension.sendMessage("set-expected", {expect, origin: location.href}); + await extension.awaitMessage("continue"); + + // We should not get events for a window. + testWindow.location = "file_image_bad.png"; + + // We should get events for the background page. + let registration = await navigator.serviceWorker.register(SimpleTest.getTestFileURL("webrequest_worker.js?test2"), {scope: "."}); + await waitForState(registration.installing, "activated"); + await extension.awaitMessage("done"); + testWindow.location = "about:blank"; + await registration.unregister(); + + await extension.unload(); +}); + +add_task(async function teardown() { + testWindow.close(); +}); +</script> +</head> +<body> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html new file mode 100644 index 0000000000..1b26a77f2b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html @@ -0,0 +1,214 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head_webrequest.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +let extensionData = { + manifest: { + permissions: ["webRequest", "webRequestBlocking", "<all_urls>", "tabs"], + }, + background() { + browser.webRequest.onBeforeRequest.addListener(details => { + browser.test.sendMessage("onBeforeRequest", details); + }, {urls: ["<all_urls>"]}, ["blocking"]); + + let tab; + browser.tabs.onCreated.addListener(newTab => { + browser.test.sendMessage("tab-created"); + tab = newTab; + }); + + browser.test.onMessage.addListener(msg => { + if (msg === "close-tab") { + browser.tabs.remove(tab.id); + browser.test.sendMessage("tab-closed"); + } + }); + }, +}; + +let expected = { + "file_simple_xhr.html": { + type: "main_frame", + toplevel: true, + }, + "file_image_good.png": { + type: "image", + toplevel: true, + origin: "file_simple_xhr.html", + }, + "example.txt": { + type: "xmlhttprequest", + toplevel: true, + origin: "file_simple_xhr.html", + }, + // sub frames will have the origin and first ancestor is the + // parent document + "file_simple_xhr_frame.html": { + type: "sub_frame", + toplevelParent: true, + origin: "file_simple_xhr.html", + parent: "file_simple_xhr.html", + }, + // a resource in a sub frame will have origin of the subframe, + // but the ancestor chain starts with the parent document + "xhr_resource": { + type: "xmlhttprequest", + origin: "file_simple_xhr_frame.html", + parent: "file_simple_xhr.html", + }, + "file_image_bad.png": { + type: "image", + depth: 2, + origin: "file_simple_xhr_frame.html", + parent: "file_simple_xhr.html", + }, + "file_simple_xhr_frame2.html": { + type: "sub_frame", + depth: 2, + origin: "file_simple_xhr_frame.html", + parent: "file_simple_xhr_frame.html", + }, + "file_image_redirect.png": { + type: "image", + depth: 2, + origin: "file_simple_xhr_frame2.html", + parent: "file_simple_xhr_frame.html", + }, + "xhr_resource_2": { + type: "xmlhttprequest", + depth: 2, + origin: "file_simple_xhr_frame2.html", + parent: "file_simple_xhr_frame.html", + }, + // This is loaded in a sandbox iframe. originUrl is not available for that, + // and requests within a sandboxed iframe will additionally have an empty + // url on their immediate parent/ancestor. + "file_simple_sandboxed_frame.html": { + type: "sub_frame", + depth: 3, + parent: "file_simple_xhr_frame2.html", + }, + "xhr_sandboxed": { + type: "xmlhttprequest", + sandboxed: true, + depth: 3, + parent: "", + }, + "file_image_great.png": { + type: "image", + sandboxed: true, + depth: 3, + parent: "", + }, + "file_simple_sandboxed_subframe.html": { + type: "sub_frame", + depth: 4, + parent: "", + }, +}; + +if (AppConstants.platform != "android") { + expected["favicon.ico"] = { + type: "image", + toplevel: true, + origin: "file_simple_xhr.html", + cached: false, + }; +} + +function checkDetails(details) { + // See bug 1471387 + if (details.originUrl == "about:newtab") { + return; + } + + let url = new URL(details.url); + let filename = url.pathname.split("/").pop(); + ok(filename in expected, `Should be expecting a request for ${filename}`); + let expect = expected[filename]; + is(expect.type, details.type, `${details.type} type matches`); + if (details.parentFrameId == -1) { + is(details.frameAncestors.length, 0, "no ancestors for main_frame requests"); + } else if (details.parentFrameId == 0) { + is(details.frameAncestors.length, 1, "one ancestors for sub_frame requests"); + } else { + ok(details.frameAncestors.length > 1, "have multiple ancestors for deep subframe requests"); + is(details.frameAncestors.length, expect.depth, "have multiple ancestors for deep subframe requests"); + } + if (details.parentFrameId > -1) { + ok(!expect.origin || details.originUrl.includes(expect.origin), "origin url is correct"); + is(details.frameAncestors[0].frameId, details.parentFrameId, "first ancestor matches request.parentFrameId"); + ok(details.frameAncestors[0].url.includes(expect.parent), "ancestor parent page correct"); + is(details.frameAncestors[details.frameAncestors.length - 1].frameId, 0, "last ancestor is always zero"); + // All our tests should be somewhere within the frame that we set topframe in the query string. That + // frame will always be the last ancestor. + ok(details.frameAncestors[details.frameAncestors.length - 1].url.includes("topframe=true"), "last ancestor is always topframe"); + } + if (expect.toplevel) { + is(details.frameId, 0, "expect load at top level"); + is(details.parentFrameId, -1, "expect top level frame to have no parent"); + } else if (details.type == "sub_frame") { + ok(details.frameId > 0, "expect sub_frame to load into a new frame"); + if (expect.toplevelParent) { + is(details.parentFrameId, 0, "expect sub_frame to have top level parent"); + is(details.frameAncestors.length, 1, "one ancestor for top sub_frame request"); + } else { + ok(details.parentFrameId > 0, "expect sub_frame to have parent"); + ok(details.frameAncestors.length > 1, "sub_frame has ancestors"); + } + expect.subframeId = details.frameId; + expect.parentId = details.parentFrameId; + } else if (expect.sandboxed) { + is(details.documentUrl, undefined, "null principal documentUrl for sandboxed request"); + } else { + // get the parent frame. + let purl = new URL(details.documentUrl); + let pfilename = purl.pathname.split("/").pop(); + let parent = expected[pfilename]; + is(details.frameId, parent.subframeId, "expect load in subframe"); + is(details.parentFrameId, parent.parentId, "expect subframe parent"); + } +} + +add_task(async function test_webRequest_main_frame() { + // Clear the image cache, since it gets in the way otherwise. + let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools); + let cache = imgTools.getImgCacheForDocument(document); + cache.clearCache(false); + function clearCache() { + ChromeUtils.import("resource://gre/modules/Services.jsm", {}).Services.cache2.clear(); + } + SpecialPowers.loadChromeScript(clearCache); + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let a = addLink(`file_simple_xhr.html?topframe=true&nocache=${Math.random()}`); + a.click(); + + for (let i = 0; i < Object.keys(expected).length; i++) { + checkDetails(await extension.awaitMessage("onBeforeRequest")); + } + + await extension.awaitMessage("tab-created"); + extension.sendMessage("close-tab"); + await extension.awaitMessage("tab-closed"); + + await extension.unload(); +}); +</script> +</head> +<body> +<div id="test">Sample text</div> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html new file mode 100644 index 0000000000..51ffc1e4f6 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html @@ -0,0 +1,223 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <script type="text/javascript" src="head_webrequest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +function getExtension() { + async function background() { + let expect; + let urls = ["*://*.example.org/tests/*"]; + browser.webRequest.onBeforeRequest.addListener(details => { + browser.test.assertEq(expect.shift(), "onBeforeRequest"); + }, {urls}, ["blocking"]); + browser.webRequest.onBeforeSendHeaders.addListener(details => { + browser.test.assertEq(expect.shift(), "onBeforeSendHeaders"); + }, {urls}, ["blocking", "requestHeaders"]); + browser.webRequest.onSendHeaders.addListener(details => { + browser.test.assertEq(expect.shift(), "onSendHeaders"); + }, {urls}, ["requestHeaders"]); + + async function testSecurityInfo(details, options) { + let securityInfo = await browser.webRequest.getSecurityInfo(details.requestId, options); + browser.test.assertTrue(securityInfo && securityInfo.state == "secure", + "security info reflects https"); + + if (options.certificateChain) { + // Some of the tests here only produce a single cert in the chain. + browser.test.assertTrue(securityInfo.certificates.length >= 1, "have certificate chain"); + } else { + browser.test.assertTrue(securityInfo.certificates.length == 1, "no certificate chain"); + } + let cert = securityInfo.certificates[0]; + let now = Date.now(); + browser.test.assertTrue(Number.isInteger(cert.validity.start), "cert start is integer"); + browser.test.assertTrue(Number.isInteger(cert.validity.end), "cert end is integer"); + browser.test.assertTrue(cert.validity.start < now, "cert start validity is correct"); + browser.test.assertTrue(now < cert.validity.end, "cert end validity is correct"); + if (options.rawDER) { + for (let cert of securityInfo.certificates) { + browser.test.assertTrue(!!cert.rawDER.length, "have rawDER"); + } + } + } + + browser.webRequest.onHeadersReceived.addListener(async (details) => { + browser.test.assertEq(expect.shift(), "onHeadersReceived"); + + // We exepect all requests to have been upgraded at this point. + browser.test.assertTrue(details.url.startsWith("https"), "connection is https"); + await testSecurityInfo(details, {}); + await testSecurityInfo(details, {certificateChain: true}); + await testSecurityInfo(details, {rawDER: true}); + await testSecurityInfo(details, {certificateChain: true, rawDER: true}); + + let headers = details.responseHeaders || []; + for (let header of headers) { + if (header.name.toLowerCase() === "strict-transport-security") { + return; + } + } + + headers.push({ + name: "Strict-Transport-Security", + value: "max-age=31536000000", + }); + return {responseHeaders: headers}; + }, {urls}, ["blocking", "responseHeaders"]); + browser.webRequest.onBeforeRedirect.addListener(details => { + browser.test.assertEq(expect.shift(), "onBeforeRedirect"); + }, {urls}); + browser.webRequest.onResponseStarted.addListener(details => { + browser.test.assertEq(expect.shift(), "onResponseStarted"); + }, {urls}); + browser.webRequest.onCompleted.addListener(details => { + browser.test.assertEq(expect.shift(), "onCompleted"); + browser.test.sendMessage("onCompleted", details.url); + }, {urls}); + browser.webRequest.onErrorOccurred.addListener(details => { + browser.test.notifyFail(`onErrorOccurred ${JSON.stringify(details)}`); + }, {urls}); + + async function onUpdated(tabId, tabInfo, tab) { + if (tabInfo.status !== "complete" || tab.url === "about:blank") { + return; + } + browser.tabs.remove(tabId); + browser.tabs.onUpdated.removeListener(onUpdated); + browser.test.sendMessage("tabs-done", tab.url); + } + browser.test.onMessage.addListener((url, expected) => { + expect = expected; + browser.tabs.onUpdated.addListener(onUpdated); + browser.tabs.create({url}); + }); + } + + let manifest = { + "permissions": [ + "tabs", + "webRequest", + "webRequestBlocking", + "<all_urls>", + ], + }; + return ExtensionTestUtils.loadExtension({ + manifest, + background, + }); +} + +// This test makes a request against a server that redirects with a 302. +add_task(async function test_hsts_request() { + const testPath = "example.org/tests/toolkit/components/extensions/test/mochitest"; + + let extension = getExtension(); + await extension.startup(); + + // simple redirect + let sample = "https://example.org/tests/toolkit/components/extensions/test/mochitest/file_sample.html"; + extension.sendMessage( + `https://${testPath}/redirect_auto.sjs?redirect_uri=${sample}`, + ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", + "onHeadersReceived", "onBeforeRedirect", "onBeforeRequest", + "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", + "onResponseStarted", "onCompleted"]); + // redirect_auto adds a query string + ok((await extension.awaitMessage("tabs-done")).startsWith(sample), "redirection ok"); + ok((await extension.awaitMessage("onCompleted")).startsWith(sample), "redirection ok"); + + // priming hsts + extension.sendMessage( + `https://${testPath}/hsts.sjs`, + ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", + "onHeadersReceived", "onResponseStarted", "onCompleted"]); + is(await extension.awaitMessage("tabs-done"), + "https://example.org/tests/toolkit/components/extensions/test/mochitest/hsts.sjs", + "hsts primed"); + is(await extension.awaitMessage("onCompleted"), + "https://example.org/tests/toolkit/components/extensions/test/mochitest/hsts.sjs"); + + // test upgrade + extension.sendMessage( + `http://${testPath}/hsts.sjs`, + ["onBeforeRequest", "onBeforeRedirect", "onBeforeRequest", + "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", + "onResponseStarted", "onCompleted"]); + is(await extension.awaitMessage("tabs-done"), + "https://example.org/tests/toolkit/components/extensions/test/mochitest/hsts.sjs", + "hsts upgraded"); + is(await extension.awaitMessage("onCompleted"), + "https://example.org/tests/toolkit/components/extensions/test/mochitest/hsts.sjs"); + + await extension.unload(); +}); + +// This test makes a priming request and adds the STS header, then tests the upgrade. +add_task(async function test_hsts_header() { + const testPath = "test1.example.org/tests/toolkit/components/extensions/test/mochitest"; + + let extension = getExtension(); + await extension.startup(); + + // priming hsts, this time there is no STS header, onHeadersReceived adds it. + let completed = extension.awaitMessage("onCompleted"); + let tabdone = extension.awaitMessage("tabs-done"); + extension.sendMessage( + `https://${testPath}/file_sample.html`, + ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", + "onHeadersReceived", "onResponseStarted", "onCompleted"]); + is(await tabdone, `https://${testPath}/file_sample.html`, "priming request done"); + is(await completed, `https://${testPath}/file_sample.html`, "priming request done"); + + // test upgrade from http to https due to onHeadersReceived adding STS header + completed = extension.awaitMessage("onCompleted"); + tabdone = extension.awaitMessage("tabs-done"); + extension.sendMessage( + `http://${testPath}/file_sample.html`, + ["onBeforeRequest", "onBeforeRedirect", "onBeforeRequest", + "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", + "onResponseStarted", "onCompleted"]); + is(await tabdone, `https://${testPath}/file_sample.html`, "hsts upgraded"); + is(await completed, `https://${testPath}/file_sample.html`, "request upgraded"); + + await extension.unload(); +}); + +add_task(async function test_nonBlocking_securityInfo() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": [ + "webRequest", + "<all_urls>", + ], + }, + async background() { + let tab; + browser.webRequest.onHeadersReceived.addListener(async (details) => { + let securityInfo = await browser.webRequest.getSecurityInfo(details.requestId, {}); + browser.test.assertTrue(!securityInfo, "securityInfo undefined on http request"); + browser.tabs.remove(tab.id); + browser.test.notifyPass("success"); + }, {urls: ["<all_urls>"], types: ["main_frame"]}); + tab = await browser.tabs.create({url: "https://example.org/tests/toolkit/components/extensions/test/mochitest/file_sample.html"}); + }, + }); + await extension.startup(); + + await extension.awaitFinish("success"); + await extension.unload(); +}); +</script> +</head> +<body> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_bypass_cors.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_bypass_cors.html new file mode 100644 index 0000000000..457d0508b7 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_bypass_cors.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1450965: Skip Cors Check for Early WebExtention Redirects </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +/* Description of the test: + * We try to Check if a WebExtention can redirect a request and bypass CORS + * We're redirecting a fetch request in onBeforeRequest + * which should not be blocked, even though we do not have + * the CORS information yet. + */ + +const WIN_URL = + "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_redirect_cors_bypass.html"; + + +add_task(async function test_webRequest_redirect_cors_bypass() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + "<all_urls>", + ], + }, + background() { + browser.webRequest.onBeforeRequest.addListener((details) => { + if (details.url.includes("file_cors_blocked.txt")) { + // File_cors_blocked does not need to exist, because we're redirecting anyway. + const testPath = "example.org/tests/toolkit/components/extensions/test/mochitest"; + let redirectUrl = `https://${testPath}/file_sample.txt`; + + // If the WebExtion cant bypass CORS, the fetch will throw a CORS-Exception + // because we do not have the CORS header yet for 'file-cors-blocked.txt' + return {redirectUrl}; + } + }, {urls: ["<all_urls>"]}, ["blocking"]); + }, + + }); + + await extension.startup(); + let win = window.open(WIN_URL); + // Creating a message channel to the new tab. + const channel = new BroadcastChannel("test_bus"); + await new Promise((resolve, reject) => { + channel.onmessage = async function(fetch_result) { + // Fetch result data will either be the text content of file_sample.txt -> 'Sample' + // or a network-Error. + // In case it's 'Sample' the redirect did happen correctly. + ok(fetch_result.data == "Sample", "Cors was Bypassed"); + win.close(); + await extension.unload(); + resolve(); + }; + }); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_data_uri.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_data_uri.html new file mode 100644 index 0000000000..5d58549c46 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_redirect_data_uri.html @@ -0,0 +1,83 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1434357: Allow Web Request API to redirect to data: URI</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +/* Description of the test: + * We load a *.js file which gets redirected to a data: URI. + * Since there is no good way to communicate loaded data: URI scripts + * we use updating a divContainer as a detour to verify the data: URI + * script has loaded. + */ + +const WIN_URL = + "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_redirect_data_uri.html"; + +add_task(async function test_webRequest_redirect_data_uri() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + "*://mochi.test/tests/*", + ], + content_scripts: [{ + matches: ["*://mochi.test/tests/*/file_redirect_data_uri.html"], + run_at: "document_end", + js: ["content_script.js"], + "all_frames": true, + }], + }, + + background() { + browser.webRequest.onBeforeRequest.addListener((details) => { + if (details.url.includes("dummy_non_existend_file.js")) { + let redirectUrl = + "data:text/javascript,document.getElementById('testdiv').textContent='loaded'"; + return {redirectUrl}; + } + }, {urls: ["*://mochi.test/tests/*"]}, ["blocking"]); + }, + + files: { + "content_script.js": function() { + let scriptEl = document.createElement("script"); + // please note that dummy_non_existend_file.js file does not really need + // to exist because we redirect the load within onBeforeRequest(). + scriptEl.src = "dummy_non_existend_file.js"; + document.body.appendChild(scriptEl); + + scriptEl.onload = function() { + let divContent = document.getElementById("testdiv").textContent; + browser.test.assertEq(divContent, "loaded", + "redirect to data: URI allowed"); + browser.test.sendMessage("finished"); + }; + scriptEl.onerror = function() { + browser.test.fail("script load failure"); + browser.test.sendMessage("finished"); + }; + }, + }, + }); + + await extension.startup(); + let win = window.open(WIN_URL); + await extension.awaitMessage("finished"); + win.close(); + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upgrade.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upgrade.html new file mode 100644 index 0000000000..3d24f5a64d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upgrade.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_webRequest_upgrade() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + "*://mochi.test/tests/*", + ], + }, + background() { + browser.webRequest.onSendHeaders.addListener((details) => { + // At this point, the request should have been upgraded. + browser.test.assertTrue(details.url.startsWith("https:"), "request is upgraded"); + browser.test.assertTrue(details.url.includes("file_sample"), "redirect after upgrade worked"); + browser.test.sendMessage("finished"); + }, {urls: ["*://mochi.test/tests/*"]}); + + browser.webRequest.onBeforeRequest.addListener((details) => { + browser.test.log(`onBeforeRequest ${details.requestId} ${details.url}`); + let url = new URL(details.url); + if (url.protocol == "http:") { + return {upgradeToSecure: true}; + } + // After the channel is initially upgraded, we get another onBeforeRequest + // call. Here we can redirect again to a new url. + if (details.url.includes("file_mixed.html")) { + let redirectUrl = new URL("file_sample.html", details.url).href; + return {redirectUrl}; + } + }, {urls: ["*://mochi.test/tests/*"]}, ["blocking"]); + }, + }); + + await extension.startup(); + let win = window.open("http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_mixed.html"); + await extension.awaitMessage("finished"); + win.close(); + await extension.unload(); +}); + +add_task(async function test_webRequest_redirect_wins() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + "*://mochi.test/tests/*", + ], + }, + background() { + browser.webRequest.onSendHeaders.addListener((details) => { + // At this point, the request should have been redirected instead of upgraded. + browser.test.assertTrue(details.url.includes("file_sample"), "request was redirected"); + browser.test.sendMessage("finished"); + }, {urls: ["*://mochi.test/tests/*"]}); + + browser.webRequest.onBeforeRequest.addListener((details) => { + if (details.url.includes("file_mixed.html")) { + let redirectUrl = new URL("file_sample.html", details.url).href; + return {upgradeToSecure: true, redirectUrl}; + } + }, {urls: ["*://mochi.test/tests/*"]}, ["blocking"]); + }, + }); + + await extension.startup(); + let win = window.open("http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_mixed.html"); + await extension.awaitMessage("finished"); + win.close(); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html new file mode 100644 index 0000000000..f4f2e66955 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html @@ -0,0 +1,212 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<form method="post" + action="file_WebRequest_page3.html?trigger=form" + target="_blank" + enctype="multipart/form-data" + > +<input type="text" name=""special" ch�rs" value="sp�cial"> +<input type="file" name="testFile"> +<input type="file" name="emptyFile"> +<input type="text" name="textInput1" value="value1"> +</form> + +<form method="post" + action="file_WebRequest_page3.html?trigger=form" + target="_blank" + enctype="multipart/form-data" + > +<input type="text" name="textInput2" value="value2"> +<input type="file" name="testFile"> +<input type="file" name="emptyFile"> +</form> + +</form> +<form method="post" + action="file_WebRequest_page3.html?trigger=form" + target="_blank" + > +<input type="text" name="textInput" value="value1"> +<input type="text" name="textInput" value="value2"> +</form> +<script> +"use strict"; + +let files, testFile, blob, file, uploads; +add_task(async function test_setup() { + files = await new Promise(resolve => { + SpecialPowers.createFiles([{name: "testFile.pdf", data: "Not really a PDF file :)", "type": "application/x-pdf"}], (result) => { + resolve(result); + }); + }); + testFile = files[0]; + blob = { + name: "blobAsFile", + content: new Blob(["A blob sent as a file"], {type: "text/csv"}), + fileName: "blobAsFile.csv", + }; + file = { + name: "testFile", + fileName: testFile.name, + }; + uploads = { + [blob.name]: blob, + [file.name]: file, + }; +}); + +function background() { + const FILTERS = {urls: ["<all_urls>"]}; + + function onUpload(details) { + let url = new URL(details.url); + let upload = url.searchParams.get("upload"); + if (!upload) { + return; + } + + let requestBody = details.requestBody; + browser.test.log(`onBeforeRequest upload: ${details.url} ${JSON.stringify(details.requestBody)}`); + browser.test.assertTrue(!!requestBody, `Intercepted upload ${details.url} #${details.requestId} ${upload} have a requestBody`); + if (!requestBody) { + return; + } + let byteLength = parseInt(upload, 10); + if (byteLength) { + browser.test.assertTrue(!!requestBody.raw, `Binary upload ${details.url} #${details.requestId} ${upload} have a raw attribute`); + browser.test.assertEq(byteLength, requestBody.raw && requestBody.raw.map(r => r.bytes ? r.bytes.byteLength : 0).reduce((a, b) => a + b), `Binary upload size matches`); + return; + } + if ("raw" in requestBody) { + browser.test.assertEq(upload, JSON.stringify(requestBody.raw).replace(/(\bfile: ")[^"]+/, "$1<file>"), `Upload ${details.url} #${details.requestId} matches raw data`); + } else { + browser.test.assertEq(upload, JSON.stringify(requestBody.formData), `Upload ${details.url} #${details.requestId} matches form data.`); + } + } + + browser.webRequest.onCompleted.addListener( + details => { + browser.test.log(`onCompleted ${details.requestId} ${details.url}`); + // See bug 1471387 + if (details.url.endsWith("/favicon.ico") || details.originUrl == "about:newtab") { + return; + } + + browser.test.sendMessage("done"); + }, + FILTERS); + + let onBeforeRequest = details => { + browser.test.log(`${name} ${details.requestId} ${details.url}`); + // See bug 1471387 + if (details.url.endsWith("/favicon.ico") || details.originUrl == "about:newtab") { + return; + } + + onUpload(details); + }; + + browser.webRequest.onBeforeRequest.addListener( + onBeforeRequest, FILTERS, ["requestBody"]); + + let tab; + browser.tabs.onCreated.addListener(newTab => { + tab = newTab; + }); + + browser.test.onMessage.addListener(msg => { + if (msg === "close-tab") { + browser.tabs.remove(tab.id); + browser.test.sendMessage("tab-closed"); + } + }); +} + +add_task(async function test_xhr_forms() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "tabs", + "webRequest", + "webRequestBlocking", + "<all_urls>", + ], + }, + background, + }); + + await extension.startup(); + + async function doneAndTabClosed() { + await extension.awaitMessage("done"); + let closed = extension.awaitMessage("tab-closed"); + extension.sendMessage("close-tab"); + await closed; + } + + for (let form of document.forms) { + if (file.name in form.elements) { + SpecialPowers.wrap(form.elements[file.name]).mozSetFileArray(files); + } + let action = new URL(form.action); + let formData = new FormData(form); + let webRequestFD = {}; + + let updateActionURL = () => { + for (let name of formData.keys()) { + webRequestFD[name] = name in uploads ? [uploads[name].fileName] : formData.getAll(name); + } + action.searchParams.set("upload", JSON.stringify(webRequestFD)); + action.searchParams.set("enctype", form.enctype); + }; + + updateActionURL(); + + form.action = action; + form.submit(); + await doneAndTabClosed(); + + if (form.enctype !== "multipart/form-data") { + continue; + } + + let post = (data) => { + let xhr = new XMLHttpRequest(); + action.searchParams.set("xhr", "1"); + xhr.open("POST", action.href); + xhr.send(data); + action.searchParams.delete("xhr"); + return doneAndTabClosed(); + }; + + formData.append(blob.name, blob.content, blob.fileName); + formData.append("formDataField", "some value"); + updateActionURL(); + await post(formData); + + action.searchParams.set("upload", JSON.stringify([{file: "<file>"}])); + await post(testFile); + + action.searchParams.set("upload", `${blob.content.size} bytes`); + await post(blob.content); + + let byteLength = 16; + action.searchParams.set("upload", `${byteLength} bytes`); + await post(new ArrayBuffer(byteLength)); + } + + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html b/toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html new file mode 100644 index 0000000000..53b19d0ead --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html @@ -0,0 +1,104 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +/* eslint-disable mozilla/balanced-listeners */ + +add_task(async function test_postMessage() { + let extensionData = { + manifest: { + content_scripts: [ + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_start", + "all_frames": true, + }, + ], + + web_accessible_resources: ["iframe.html"], + }, + + background() { + browser.test.sendMessage("iframe-url", browser.runtime.getURL("iframe.html")); + }, + + files: { + "content_script.js": function() { + window.addEventListener("message", event => { + if (event.data == "ping") { + event.source.postMessage({pong: location.href}, + event.origin); + } + }); + }, + + "iframe.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <script src="content_script.js"><\/script> + </head> + </html>`, + }, + }; + + let createIframe = url => { + let iframe = document.createElement("iframe"); + return new Promise(resolve => { + iframe.src = url; + iframe.onload = resolve; + document.body.appendChild(iframe); + }).then(() => { + return iframe; + }); + }; + + let awaitMessage = () => { + return new Promise(resolve => { + let listener = event => { + if (event.data.pong) { + window.removeEventListener("message", listener); + resolve(event.data); + } + }; + window.addEventListener("message", listener); + }); + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + + let iframeURL = await extension.awaitMessage("iframe-url"); + let testURL = SimpleTest.getTestFileURL("file_sample.html"); + + for (let url of [iframeURL, testURL]) { + info(`Testing URL ${url}`); + + let iframe = await createIframe(url); + + iframe.contentWindow.postMessage( + "ping", url); + + let pong = await awaitMessage(); + is(pong.pong, url, "Got expected pong"); + + iframe.remove(); + } + + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_verify_non_remote_mode.html b/toolkit/components/extensions/test/mochitest/test_verify_non_remote_mode.html new file mode 100644 index 0000000000..6f46fa8eea --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_verify_non_remote_mode.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Verify non-remote mode</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; +add_task(async function verify_extensions_in_parent_process() { + // This test ensures we are running with the proper settings. + const { WebExtensionPolicy } = SpecialPowers.Cu.getGlobalForObject(SpecialPowers.Services); + SimpleTest.ok(!WebExtensionPolicy.useRemoteWebExtensions, "extensions running in-process"); + + let chromeScript = SpecialPowers.loadChromeScript(() => { + const { WebExtensionPolicy } = Cu.getGlobalForObject(Services); + Assert.ok(WebExtensionPolicy.isExtensionProcess, "parent is extension process"); + this.sendAsyncMessage("checks_done"); + }); + await chromeScript.promiseOneMessage("checks_done"); + chromeScript.destroy(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_verify_remote_mode.html b/toolkit/components/extensions/test/mochitest/test_verify_remote_mode.html new file mode 100644 index 0000000000..2be0e19179 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_verify_remote_mode.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Verify remote mode</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> + "use strict"; + // This test ensures we are running with the proper settings. + const {WebExtensionPolicy} = SpecialPowers.Cu.getGlobalForObject(SpecialPowers.Services); + SimpleTest.ok(WebExtensionPolicy.useRemoteWebExtensions, "extensions running remote"); + SimpleTest.ok(!WebExtensionPolicy.isExtensionProcess, "testing from remote process"); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js b/toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js new file mode 100644 index 0000000000..6a44fcac2e --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js @@ -0,0 +1,9 @@ +"use strict"; + +/* eslint-env worker */ + +onmessage = function(event) { + fetch("https://example.com/example.txt").then(() => { + postMessage("Done!"); + }); +}; diff --git a/toolkit/components/extensions/test/mochitest/webrequest_test.jsm b/toolkit/components/extensions/test/mochitest/webrequest_test.jsm new file mode 100644 index 0000000000..6fc2fe3d7f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/webrequest_test.jsm @@ -0,0 +1,22 @@ +"use strict"; + +var EXPORTED_SYMBOLS = ["webrequest_test"]; + +Cu.importGlobalProperties(["fetch"]); + +var webrequest_test = { + testFetch(url) { + return fetch(url); + }, + + testXHR(url) { + return new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.open("HEAD", url); + xhr.onload = () => { + resolve(); + }; + xhr.send(); + }); + }, +}; diff --git a/toolkit/components/extensions/test/mochitest/webrequest_worker.js b/toolkit/components/extensions/test/mochitest/webrequest_worker.js new file mode 100644 index 0000000000..dcffd08578 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/webrequest_worker.js @@ -0,0 +1,3 @@ +"use strict"; + +fetch("https://example.com/example.txt"); |