diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /toolkit/components/extensions/test | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/components/extensions/test')
180 files changed, 20067 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 000000000..53938410b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/.eslintrc.js @@ -0,0 +1,35 @@ +"use strict"; + +module.exports = { // eslint-disable-line no-undef + "extends": "../../../../../testing/mochitest/mochitest.eslintrc.js", + + "env": { + "webextensions": true, + }, + + "globals": { + "ChromeWorker": false, + "onmessage": true, + "sendAsyncMessage": false, + + "waitForLoad": true, + "promiseConsoleOutput": true, + + "ExtensionTestUtils": false, + "NetUtil": true, + "webrequest_test": false, + "XPCOMUtils": true, + + // head_webrequest.js symbols + "addStylesheet": true, + "addLink": true, + "addImage": true, + "addScript": true, + "addFrame": true, + "makeExtension": false, + }, + + "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 000000000..26585cad7 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/chrome.ini @@ -0,0 +1,35 @@ +[DEFAULT] +support-files = + chrome_head.js + head.js + head_cookies.js + file_sample.html + webrequest_chromeworker.js + webrequest_test.jsm +tags = webextensions + +[test_chrome_ext_background_debug_global.html] +skip-if = (os == 'android') # android doesn't have devtools +[test_chrome_ext_background_page.html] +skip-if = (toolkit == 'android') # android doesn't have devtools +[test_chrome_ext_eventpage_warning.html] +[test_chrome_ext_contentscript_unrecognizedprop_warning.html] +skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android. +[test_chrome_ext_hybrid_addons.html] +[test_chrome_ext_trustworthy_origin.html] +[test_chrome_ext_webnavigation_resolved_urls.html] +skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android. +[test_chrome_ext_shutdown_cleanup.html] +[test_chrome_native_messaging_paths.html] +skip-if = os != "mac" && os != "linux" +[test_ext_cookies_expiry.html] +[test_ext_cookies_permissions_bad.html] +[test_ext_cookies_permissions_good.html] +[test_ext_cookies_containers.html] +[test_ext_jsversion.html] +[test_ext_schema.html] +[test_chrome_ext_storage_cleanup.html] +[test_chrome_ext_idle.html] +[test_chrome_ext_downloads_saveAs.html] +[test_chrome_ext_webrequest_background_events.html] +skip-if = os == 'android' # webrequest api unsupported (bug 1258975). 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 000000000..da2f53a02 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/chrome_head.js @@ -0,0 +1,12 @@ +"use strict"; + +const { + classes: Cc, + interfaces: Ci, + utils: Cu, + results: Cr, +} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + 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 000000000..663ebc611 --- /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 000000000..cc1acc83d --- /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 000000000..a0a26a2e9 --- /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 000000000..5807dd439 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +<script> +"use strict"; +window.close(); +</script> +</head> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_csp.html b/toolkit/components/extensions/test/mochitest/file_csp.html new file mode 100644 index 000000000..206e44390 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_csp.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<div id="test">Sample text</div> +<img id="bad-image" src="http://example.org/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png" /> +<script id="bad-script" type="text/javascript" src="http://example.org/tests/toolkit/components/extensions/test/mochitest/file_script_bad.js"></script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_csp.html^headers^ b/toolkit/components/extensions/test/mochitest/file_csp.html^headers^ new file mode 100644 index 000000000..4c6fa3c26 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_csp.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' diff --git a/toolkit/components/extensions/test/mochitest/file_ext_test_api_injection.js b/toolkit/components/extensions/test/mochitest/file_ext_test_api_injection.js new file mode 100644 index 000000000..06dfae65e --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_ext_test_api_injection.js @@ -0,0 +1,12 @@ +"use strict"; + +var {interfaces: Ci} = Components; + +Components.utils.import("resource://gre/modules/Services.jsm"); + +Services.console.registerListener(function listener(message) { + if (/WebExt Privilege Escalation/.test(message.message)) { + Services.console.unregisterListener(listener); + sendAsyncMessage("console-message", {message: message.message}); + } +}); 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 000000000..4c3be5084 --- /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 000000000..769c63634 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_image_good.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 000000000..4c3be5084 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_image_redirect.png 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 000000000..f3c7dda58 --- /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_permission_xhr.html b/toolkit/components/extensions/test/mochitest/file_permission_xhr.html new file mode 100644 index 000000000..22a55f90d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_permission_xhr.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<script> +"use strict"; + +/* globals privilegedFetch, privilegedXHR */ +/* eslint-disable mozilla/balanced-listeners */ + +addEventListener("message", function rcv(event) { + removeEventListener("message", rcv, false); + + function assertTrue(condition, description) { + postMessage({msg: "assertTrue", condition, description}, "*"); + } + + function passListener() { + assertTrue(true, "Content XHR has no elevated privileges"); + postMessage({"msg": "finish"}, "*"); + } + + function failListener() { + assertTrue(false, "Content XHR has no elevated privileges"); + postMessage({"msg": "finish"}, "*"); + } + + try { + new privilegedXHR(); + assertTrue(false, "Content should not have access to privileged XHR constructor"); + } catch (e) { + assertTrue(/Permission denied to access object/.test(e), "Content should not have access to privileged XHR constructor"); + } + + try { + new privilegedFetch(); + assertTrue(false, "Content should not have access to privileged fetch() constructor"); + } catch (e) { + assertTrue(/Permission denied to access object/.test(e), "Content should not have access to privileged fetch() constructor"); + } + + let req = new XMLHttpRequest(); + req.addEventListener("load", failListener); + req.addEventListener("error", passListener); + req.open("GET", "http://example.org/example.txt"); + req.send(); +}, false); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/file_privilege_escalation.html b/toolkit/components/extensions/test/mochitest/file_privilege_escalation.html new file mode 100644 index 000000000..258f7058d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_privilege_escalation.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + <script type="text/javascript"> + "use strict"; + throw new Error(`WebExt Privilege Escalation: typeof(browser) = ${typeof(browser)}`); + </script> +</body> +</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 000000000..a20e49a1f --- /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_script_bad.js b/toolkit/components/extensions/test/mochitest/file_script_bad.js new file mode 100644 index 000000000..c425122c7 --- /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 000000000..1848edf68 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_script_good.js @@ -0,0 +1,3 @@ +"use strict"; + +window.success = window.success ? window.success + 1 : 1; 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 000000000..c89a196c2 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_script_redirect.js @@ -0,0 +1,4 @@ +"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 000000000..07f80eb2e --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_script_xhr.js @@ -0,0 +1,5 @@ +"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_style_bad.css b/toolkit/components/extensions/test/mochitest/file_style_bad.css new file mode 100644 index 000000000..8dbc8dc7a --- /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 000000000..46f9774b5 --- /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 000000000..8dbc8dc7a --- /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_teardown_test.js b/toolkit/components/extensions/test/mochitest/file_teardown_test.js new file mode 100644 index 000000000..7246012ad --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_teardown_test.js @@ -0,0 +1,24 @@ +"use strict"; + +/* globals addMessageListener */ +let {Management} = Components.utils.import("resource://gre/modules/Extension.jsm", {}); +let events = []; +function record(type, extensionContext) { + let eventType = type == "proxy-context-load" ? "load" : "unload"; + let url = extensionContext.uri.spec; + let extensionId = extensionContext.extension.id; + events.push({eventType, url, extensionId}); +} + +Management.on("proxy-context-load", record); +Management.on("proxy-context-unload", record); +addMessageListener("cleanup", () => { + Management.off("proxy-context-load", record); + Management.off("proxy-context-unload", record); +}); + +addMessageListener("get-context-events", extensionId => { + sendAsyncMessage("context-events", events); + events = []; +}); +sendAsyncMessage("chromescript-startup"); 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 000000000..cba3043f7 --- /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 000000000..c5b436979 --- /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 000000000..574a392a1 --- /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 000000000..d360bcbb1 --- /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 000000000..06dbd4374 --- /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 000000000..307990714 --- /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 000000000..55bb7aa6a --- /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 000000000..8f589f8bb --- /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 000000000..af51c2e52 --- /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/head.js b/toolkit/components/extensions/test/mochitest/head.js new file mode 100644 index 000000000..1b1a29472 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head.js @@ -0,0 +1,13 @@ +"use strict"; + +/* exported waitForLoad */ + +function waitForLoad(win) { + return new Promise(resolve => { + win.addEventListener("load", function listener() { + win.removeEventListener("load", listener, true); + resolve(); + }, true); + }); +} + 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 000000000..9f6966551 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head_cookies.js @@ -0,0 +1,167 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* exported testCookies */ + +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 cookieSvc = SpecialPowers.Services.cookies; + + let domain = options.domain.replace(/^\.?/, "."); + + // This will be evicted after we add a fourth cookie. + cookieSvc.add(domain, "/", "evicted", "bar", options.secure, false, false, options.expiry); + // This will be modified by the background script. + cookieSvc.add(domain, "/", "foo", "bar", options.secure, false, false, options.expiry); + // This will be deleted by the background script. + cookieSvc.add(domain, "/", "deleted", "bar", options.secure, false, false, options.expiry); + + + yield extension.startup(); + + yield extension.awaitMessage("change-cookies"); + cookieSvc.add(domain, "/", "x", "y", options.secure, false, false, options.expiry); + cookieSvc.add(domain, "/", "x", "z", options.secure, false, false, options.expiry); + cookieSvc.remove(domain, "x", "/", false, {}); + extension.sendMessage("cookies-changed"); + + yield extension.awaitFinish("cookie-permissions"); + yield extension.unload(); + + + function getCookies(host) { + let cookies = []; + let enum_ = cookieSvc.getCookiesFromHost(host, {}); + while (enum_.hasMoreElements()) { + cookies.push(enum_.getNext().QueryInterface(SpecialPowers.Ci.nsICookie2)); + } + return cookies.sort((a, b) => String.localeCompare(a.name, b.name)); + } + + let cookies = getCookies(options.domain); + info(`Cookies: ${cookies.map(c => `${c.name}=${c.value}`)}`); + + if (options.shouldPass) { + is(cookies.length, 2, "expected two cookies for host"); + + is(cookies[0].name, "bar", "correct cookie name"); + is(cookies[0].value, "quux", "correct cookie value"); + + is(cookies[1].name, "foo", "correct cookie name"); + is(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. + is(cookies.length, 3, "expected three cookies for host"); + + is(cookies[0].name, "bar", "correct cookie name"); + is(cookies[0].value, "quux", "correct cookie value"); + + is(cookies[1].name, "deleted", "correct cookie name"); + + is(cookies[2].name, "foo", "correct cookie name"); + is(cookies[2].value, "baz", "correct cookie value"); + } else { + is(cookies.length, 2, "expected two cookies for host"); + + is(cookies[0].name, "deleted", "correct second cookie name"); + + is(cookies[1].name, "foo", "correct cookie name"); + is(cookies[1].value, "bar", "correct cookie value"); + } + + for (let cookie of cookies) { + cookieSvc.remove(cookie.host, cookie.name, "/", false, {}); + } + // Make sure we don't silently poison subsequent tests if something goes wrong. + is(getCookies(options.domain).length, 0, "cookies cleared"); +} 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 000000000..96924e505 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/head_webrequest.js @@ -0,0 +1,331 @@ +"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"]], + "onResponseStarted": [{urls: ["<all_urls>"]}], + "onCompleted": [{urls: ["<all_urls>"]}, ["responseHeaders"]], + "onErrorOccurred": [{urls: ["<all_urls>"]}], +}; + +function background(events) { + let expect; + let ignore; + let defaultOrigin; + + 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; + if (url.protocol == "data:") { + // pathname is everything after protocol. + filename = url.pathname; + } else { + 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`); + } + } + + 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 { + expectedEvent = expected.optional_events[0] == name; + if (expectedEvent) { + expected.optional_events.shift(); + } + } + 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") { + // 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.assertNotEq(expected.test.requestId, details.requestId, + `last requestId ${expected.test.requestId} different from this one ${details.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 bug 1329299"); + } + } else { + // 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"); + } + if (name == "onBeforeSendHeaders") { + 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); + } + } + if (name == "onSendHeaders") { + if (expected.headers && expected.headers.request) { + checkHeaders("request", expected, details); + } + } + if (name == "onHeadersReceived") { + browser.test.assertEq(expected.status || 200, details.statusCode, + `expected HTTP status received for ${details.url}`); + if (expected.headers && expected.headers.response) { + result.responseHeaders = processHeaders("response", expected, details); + } + } + if (name == "onCompleted") { + // 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. + 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.assertEq("127.0.0.1", details.ip, `correct ip for ${details.url}`); + } + if (expected.headers && expected.headers.response) { + checkHeaders("response", expected, details); + } + } + + 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"); + 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/mochitest.ini b/toolkit/components/extensions/test/mochitest/mochitest.ini new file mode 100644 index 000000000..45586237e --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/mochitest.ini @@ -0,0 +1,114 @@ +[DEFAULT] +support-files = + head.js + file_mixed.html + head_webrequest.js + file_csp.html + file_csp.html^headers^ + file_WebRequest_page3.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_WebNavigation_page1.html + file_WebNavigation_page2.html + file_WebNavigation_page3.html + file_with_about_blank.html + file_image_good.png + file_image_bad.png + file_image_redirect.png + file_style_good.css + file_style_bad.css + file_style_redirect.css + file_script_good.js + file_script_bad.js + file_script_redirect.js + file_script_xhr.js + file_sample.html + redirection.sjs + file_privilege_escalation.html + file_ext_test_api_injection.js + file_permission_xhr.html + file_teardown_test.js + return_headers.sjs + webrequest_worker.js +tags = webextensions + +[test_clipboard.html] +# skip-if = # disabled test case with_permission_allow_copy, see inline comment. +[test_ext_inIncognitoContext_window.html] +skip-if = os == 'android' # Android does not currently support windows. +[test_ext_geturl.html] +[test_ext_background_canvas.html] +[test_ext_content_security_policy.html] +[test_ext_contentscript.html] +[test_ext_contentscript_api_injection.html] +[test_ext_contentscript_async_loading.html] +[test_ext_contentscript_context.html] +[test_ext_contentscript_create_iframe.html] +[test_ext_contentscript_devtools_metadata.html] +[test_ext_contentscript_exporthelpers.html] +[test_ext_contentscript_css.html] +[test_ext_contentscript_about_blank.html] +[test_ext_contentscript_permission.html] +skip-if = os == 'android' # Android does not support tabs API. Bug 1260250 +[test_ext_contentscript_teardown.html] +skip-if = (os == 'android') # Android does not support tabs API. Bug 1260250 +[test_ext_exclude_include_globs.html] +[test_ext_i18n_css.html] +[test_ext_generate.html] +[test_ext_notifications.html] +[test_ext_permission_xhr.html] +[test_ext_runtime_connect.html] +skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). +[test_ext_runtime_connect_twoway.html] +skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). +[test_ext_runtime_connect2.html] +skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). +[test_ext_runtime_disconnect.html] +[test_ext_runtime_id.html] +[test_ext_sandbox_var.html] +[test_ext_sendmessage_reply.html] +skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). +[test_ext_sendmessage_reply2.html] +skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). +[test_ext_sendmessage_doublereply.html] +skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). +[test_ext_sendmessage_no_receiver.html] +[test_ext_storage_content.html] +[test_ext_storage_tab.html] +skip-if = os == 'android' # Android does not currently support tabs. +[test_ext_test.html] +[test_ext_cookies.html] +skip-if = os == 'android' # Bug 1258975 on android. +[test_ext_background_api_injection.html] +[test_ext_background_generated_url.html] +[test_ext_background_teardown.html] +[test_ext_tab_teardown.html] +skip-if = (os == 'android') # Android does not support tabs API. Bug 1260250 +[test_ext_unload_frame.html] +[test_ext_i18n.html] +skip-if = (os == 'android') # Bug 1258975 on android. +[test_ext_listener_proxies.html] +[test_ext_web_accessible_resources.html] +skip-if = (os == 'android') # Bug 1258975 on android. +[test_ext_webrequest_background_events.html] +skip-if = os == 'android' # webrequest api unsupported (bug 1258975). +[test_ext_webrequest_basic.html] +skip-if = os == 'android' # webrequest api unsupported (bug 1258975). +[test_ext_webrequest_suspend.html] +skip-if = os == 'android' # webrequest api unsupported (bug 1258975). +[test_ext_webrequest_upload.html] +skip-if = release_or_beta || os == 'android' # webrequest api unsupported (bug 1258975). +[test_ext_webnavigation.html] +skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). +[test_ext_webnavigation_filters.html] +skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). +[test_ext_window_postMessage.html] +[test_ext_subframes_privileges.html] +skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). +[test_ext_xhr_capabilities.html] diff --git a/toolkit/components/extensions/test/mochitest/redirection.sjs b/toolkit/components/extensions/test/mochitest/redirection.sjs new file mode 100644 index 000000000..370ecd213 --- /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 000000000..54e2e5fb4 --- /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/test_chrome_ext_background_debug_global.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html new file mode 100644 index 000000000..0edf5ea86 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html @@ -0,0 +1,166 @@ +<!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/SpawnTask.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="chrome_head.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm"); +Cu.import("resource://gre/modules/AddonManager.jsm"); + +const { + XPIProvider, +} = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm"); + +/** + * This test is asserting that ext-backgroundPage.js successfully sets its + * debug global in the AddonWrapper provided by XPIProvider.jsm + * + * It does _not_ test any functionality in devtools and does not guarantee + * debugging is actually working correctly end-to-end. + */ + +function background() { + window.testThing = "test!"; + browser.test.notifyPass("background script ran"); +} + +const ID = "debug@tests.mozilla.org"; +let extensionData = { + useAddonManager: "temporary", + background, + manifest: { + applications: {gecko: {id: ID}}, + }, +}; + +add_task(function* () { + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + yield extension.awaitFinish("background script ran"); + + yield new Promise(function(resolve) { + window.BrowserToolboxProcess.emit("connectionchange", "opened", { + setAddonOptions(id, options) { + if (id === ID) { + let context = Cu.waiveXrays(options.global); + ok(context.chrome, "global context has a chrome object"); + ok(context.browser, "global context has a browser object"); + is("test!", context.testThing, "global context is the background script context"); + resolve(); + } + }, + }); + }); + + let addon = yield new Promise((resolve, reject) => { + AddonManager.getAddonByID(ID, addon => addon ? resolve(addon) : reject()); + }); + + ok(addon, `Got the addon wrapper for ${addon.id}`); + + function waitForDebugGlobalChanges(times, initialAddonInstanceID) { + return new Promise((resolve) => { + AddonManager.addAddonListener({ + count: 0, + notNullGlobalsCount: 0, + undefinedPrivateWrappersCount: 0, + lastAddonInstanceID: initialAddonInstanceID, + onPropertyChanged(newAddon, changedPropNames) { + if (newAddon.id != addon.id || + !changedPropNames.includes("debugGlobal")) { + return; + } + + ok(!(newAddon.setDebugGlobal) && !(newAddon.getDebugGlobal), + "The addon wrapper should not be a PrivateWrapper"); + + let activeAddon = XPIProvider.activeAddons.get(addon.id); + + let addonInstanceID; + + if (!activeAddon) { + // The addon has been disable, the preferred global should be null + addonInstanceID = this.lastAddonInstanceID; + delete this.lastAddonInstanceID; + } else { + addonInstanceID = activeAddon.instanceID; + this.lastAddonInstanceID = addonInstanceID; + } + + ok(addonInstanceID, `Got the addon instanceID for ${addon.id}`); + + AddonManager.getAddonByInstanceID(addonInstanceID).then((privateWrapper) => { + this.count += 1; + + if (!privateWrapper) { + // The addon has been uninstalled + this.undefinedPrivateWrappersCount += 1; + } else { + ok((privateWrapper.getDebugGlobal), "Got the addon PrivateWrapper"); + + if (privateWrapper.getDebugGlobal()) { + this.notNullGlobalsCount += 1; + } + } + + if (this.count == times) { + AddonManager.removeAddonListener(this); + resolve({ + counters: { + count: this.count, + notNullGlobalsCount: this.notNullGlobalsCount, + undefinedPrivateWrappersCount: this.undefinedPrivateWrappersCount, + }, + lastAddonInstanceID: this.lastAddonInstanceID, + }); + } + }); + }, + }); + }); + } + + // two calls expected, one for the shutdown and one for the startup + // of the background page. + let waitForDebugGlobalChangesOnReload = waitForDebugGlobalChanges(2); + + info("Addon reload..."); + yield addon.reload(); + + info("Addon completed startup after reload"); + + let { + counters: reloadCounters, + lastAddonInstanceID, + } = yield waitForDebugGlobalChangesOnReload; + + isDeeply(reloadCounters, {count: 2, notNullGlobalsCount: 1, undefinedPrivateWrappersCount: 0}, + "Got the expected number of onPropertyChanged calls on reload"); + + // one more call expected for the shutdown. + let waitForDebugGlobalChangesOnShutdown = waitForDebugGlobalChanges(1, lastAddonInstanceID); + + info("extension unloading..."); + yield extension.unload(); + info("extension unloaded"); + + let {counters: unloadCounters} = yield waitForDebugGlobalChangesOnShutdown; + + isDeeply(unloadCounters, {count: 1, notNullGlobalsCount: 0, undefinedPrivateWrappersCount: 1}, + "Got the expected number of onPropertyChanged calls on shutdown"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_page.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_page.html new file mode 100644 index 000000000..3c4774652 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_page.html @@ -0,0 +1,84 @@ +<!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/SpawnTask.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="chrome_head.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +Cu.import("resource://testing-common/TestUtils.jsm"); + +/* eslint-disable mozilla/balanced-listeners */ + +add_task(function* testAlertNotShownInBackgroundWindow() { + ok(!Services.wm.getEnumerator("alert:alert").hasMoreElements(), + "Alerts should not be present at the start of the test."); + + let consoleOpened = TestUtils.topicObserved("web-console-created"); + + + let extension = ExtensionTestUtils.loadExtension({ + background: function() { + browser.test.log("background script executed"); + + alert("I am an alert in the background."); + + browser.test.notifyPass("alertCalled"); + }, + }); + + yield extension.startup(); + + info("startup complete loaded"); + + yield extension.awaitFinish("alertCalled"); + + + let alertWindows = Services.wm.getEnumerator("alert:alert"); + ok(!alertWindows.hasMoreElements(), "Should not show alert"); + + + // Make sure the message we output to the console is seen. + // This message is in ext-backgroundPage.js + let events = Cc["@mozilla.org/consoleAPI-storage;1"] + .getService(Ci.nsIConsoleAPIStorage).getEvents(); + + // This is the warning that is output after the first `alert()` call is made. + let alertWarningEvent = events[events.length - 2]; + is(alertWarningEvent.arguments[0], "alert() is not supported in background windows; please use console.log instead."); + + // This is the actual alert text that should be present in the console + // instead of as an `alert`. + let alertEvent = events[events.length - 1]; + is(alertEvent.arguments[0], "I am an alert in the background."); + + + // Wait for the browser console window to open. + yield consoleOpened; + + let {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); + require("devtools/client/framework/devtools-browser"); + let hudservice = require("devtools/client/webconsole/hudservice"); + + // And then double check that we have an actual browser console. + let haveConsole = !!hudservice.getBrowserConsole(); + ok(haveConsole, "Expected browser console to be open"); + + if (haveConsole) { + yield hudservice.toggleBrowserConsole(); + } + + yield 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 000000000..e08121a8f --- /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/SpawnTask.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="chrome_head.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/contents/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(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: Error processing content_scripts.*.unrecognized_property: An unexpected property was found/, + }]); + }); + + yield extension.startup(); + + window.open(`${BASE}/file_sample.html`); + + yield Promise.all([extension.awaitFinish("content-script-loaded")]); + info("test page loaded"); + + yield extension.unload(); + + SimpleTest.endMonitorConsole(); + yield waitForConsole; +}); +</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 000000000..c1aaae035 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html @@ -0,0 +1,68 @@ +<!doctype html> +<html> +<head> + <title>Test downloads.download() saveAs option</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(function* test_downloads_saveAs() { + function background() { + const url = URL.createObjectURL(new Blob(["file content"])); + browser.test.onMessage.addListener(async () => { + try { + let id = await browser.downloads.download({url, saveAs: true}); + browser.downloads.onChanged.addListener(delta => { + if (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 {MockFilePicker} = SpecialPowers; + const manifest = {background, manifest: {permissions: ["downloads"]}}; + const extension = ExtensionTestUtils.loadExtension(manifest); + + MockFilePicker.init(window); + MockFilePicker.useAnyFile(); + const [file] = MockFilePicker.returnFiles; + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + extension.sendMessage("download"); + let result = yield extension.awaitMessage("done"); + + ok(result.ok, "downloads.download() works with saveAs"); + is(file.fileSize, 12, "downloaded file is the correct size"); + file.remove(false); + + // Test the user canceling the save dialog. + MockFilePicker.returnValue = MockFilePicker.returnCancel; + + extension.sendMessage("download"); + result = yield 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(!file.exists(), "file was not downloaded"); + + yield extension.unload(); + MockFilePicker.cleanup(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_eventpage_warning.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_eventpage_warning.html new file mode 100644 index 000000000..ecea8237e --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_eventpage_warning.html @@ -0,0 +1,106 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebExtension EventPage Warning</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="chrome_head.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function createEventPageExtension(eventPage) { + function eventPageScript() { + browser.test.log("running event page as background script"); + browser.test.sendMessage("running", 1); + } + + return ExtensionTestUtils.loadExtension({ + manifest: { + "background": eventPage, + }, + files: { + "event-page-script.js": eventPageScript, + "event-page.html": `<html><head> + <meta charset="utf-8"> + <script src="event-page-script.js"><\/script> + </head></html>`, + }, + }); +} + +add_task(function* test_eventpages() { + // Used in other tests to prevent the monitorConsole to grip. + SimpleTest.waitForExplicitFinish(); + + let testCases = [ + { + message: "testing event page running as a background page", + eventPage: { + "page": "event-page.html", + "persistent": false, + }, + }, + { + message: "testing event page scripts running as a background page", + eventPage: { + "scripts": ["event-page-script.js"], + "persistent": false, + }, + }, + ]; + + for (let {message, eventPage} of testCases) { + info(message); + + // Wait for the expected logged warnings from the manifest validation. + let waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, [{message: /Event pages are not currently supported./}]); + }); + + let extension = createEventPageExtension(eventPage); + + info("load complete"); + let [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]); + is(x, 1, "got correct value from extension"); + info("test complete"); + yield extension.unload(); + info("extension unloaded successfully"); + + SimpleTest.endMonitorConsole(); + yield waitForConsole; + + waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, [{ + message: /Reading manifest: Error processing background.nonExistentProp: An unexpected property was found/, + }]); + }); + + info("testing additional unrecognized properties on background page"); + + extension = createEventPageExtension({ + "scripts": ["event-page-script.js"], + "nonExistentProp": true, + }); + + info("load complete"); + [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]); + is(x, 1, "got correct value from extension"); + info("test complete"); + yield extension.unload(); + info("extension unloaded successfully"); + + SimpleTest.endMonitorConsole(); + yield waitForConsole; + } +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_hybrid_addons.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_hybrid_addons.html new file mode 100644 index 000000000..a74c551f0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_hybrid_addons.html @@ -0,0 +1,141 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for hybrid addons: SDK or bootstrap.js + embedded WebExtension</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.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/contents/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +/** + * This test contains additional tests that ensure that an SDK hybrid addon + * which is using the new module loader can embed a webextension correctly: + * + * while the other tests related to the "Embedded WebExtension" are focused + * on unit testing a specific component, these tests are testing that a complete + * hybrid SDK addon works as expected. + * + * NOTE: this tests are also the only ones which tests an SDK hybrid addon that + * uses the new module loader (the one actually used in production by real world + * addons these days), while the Addon SDK "embedded-webextension" test addon + * uses the old deprecated module loader (as all the other Addon SDK test addons). + */ + +function generateClassicExtensionFiles({id, files}) { + // The addon install.rdf file, as it would be generated by jpm from the addon + // package.json metadata. + files["install.rdf"] = `<?xml version="1.0" encoding="utf-8"?> + <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>${id}</em:id> + <em:type>2</em:type> + <em:bootstrap>true</em:bootstrap> + <em:hasEmbeddedWebExtension>true</em:hasEmbeddedWebExtension> + <em:unpack>false</em:unpack> + <em:version>0.1.0</em:version> + <em:name>Fake Hybrid Addon</em:name> + <em:description>A fake hybrid addon</em:description> + + <!-- Firefox --> + <em:targetApplication> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>51.0a1</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + + <!-- Fennec --> + <em:targetApplication> + <Description> + <em:id>{aa3c5121-dab2-40e2-81ca-7ea25febc110}</em:id> + <em:minVersion>51.0a1</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> + </RDF>`; + + // The addon package.json file. + files["package.json"] = `{ + "id": "${id}", + "name": "hybrid-addon", + "version": "0.1.0", + "description": "A fake hybrid addon", + "main": "index.js", + "engines": { + "firefox": ">= 51.0a1", + "fennec": ">= 51.0a1" + }, + "license": "MPL-2.0", + "hasEmbeddedWebExtension": true + }`; + + // The bootstrap file that jpm bundle in any SDK addon built with it. + files["bootstrap.js"] = ` + const { utils: Cu } = Components; + const rootURI = __SCRIPT_URI_SPEC__.replace("bootstrap.js", ""); + const COMMONJS_URI = "resource://gre/modules/commonjs"; + const { require } = Cu.import(COMMONJS_URI + "/toolkit/require.js", {}); + const { Bootstrap } = require(COMMONJS_URI + "/sdk/addon/bootstrap.js"); + var { startup, shutdown, install, uninstall } = new Bootstrap(rootURI); + `; + + return files; +} + +add_task(function* test_sdk_hybrid_addon_with_jpm_module_loader() { + function backgroundScript() { + browser.runtime.sendMessage("background message", (reply) => { + browser.test.assertEq("sdk received message: background message", reply, + "Got the expected reply from the SDK context"); + browser.test.notifyPass("sdk.webext-api.onmessage"); + }); + } + + async function sdkMainScript() { + /* globals require */ + const webext = require("sdk/webextension"); + let {browser} = await webext.startup(); + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + sendReply(`sdk received message: ${msg}`); + }); + } + + let id = "fake@sdk.hybrid.addon"; + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + files: generateClassicExtensionFiles({ + id, + files: { + "index.js": sdkMainScript, + "webextension/manifest.json": { + name: "embedded webextension name", + manifest_version: 2, + version: "0.1.0", + background: { + scripts: ["bg.js"], + }, + }, + "webextension/bg.js": backgroundScript, + }, + }), + }, id); + + extension.startup(); + + yield extension.awaitFinish("sdk.webext-api.onmessage"); + + yield extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_idle.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_idle.html new file mode 100644 index 000000000..3c3063e67 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_idle.html @@ -0,0 +1,64 @@ +<!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/SpawnTask.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="chrome_head.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const idleService = Cc["@mozilla.org/widget/idleservice;1"].getService(Ci.nsIIdleService); + +add_task(function* testWithRealIdleService() { + function background() { + browser.test.onMessage.addListener((msg, ...args) => { + let detectionInterval = args[0]; + if (msg == "addListener") { + browser.idle.queryState(detectionInterval).then(status => { + 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"); + }); + } else if (msg == "checkState") { + browser.idle.queryState(detectionInterval).then(status => { + browser.test.assertEq("idle", status, "Idle status is idle"); + browser.test.notifyPass("idle"); + }); + } + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["idle"], + }, + }); + + yield extension.startup(); + let idleTime = idleService.idleTime; + let detectionInterval = Math.max(Math.ceil(idleTime / 1000) + 2, 15); + info(`idleTime: ${idleTime}, detectionInterval: ${detectionInterval}`); + extension.sendMessage("addListener", detectionInterval); + info("Listener added"); + yield extension.awaitMessage("listenerFired"); + info("Listener fired"); + extension.sendMessage("checkState", detectionInterval); + yield extension.awaitFinish("idle"); + yield extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html new file mode 100644 index 000000000..e3098e6b1 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html @@ -0,0 +1,50 @@ +<!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/SpawnTask.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/contents/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://testing-common/TestUtils.jsm"); + +const {GlobalManager} = Cu.import("resource://gre/modules/Extension.jsm"); + +/* eslint-disable mozilla/balanced-listeners */ + +add_task(function* testShutdownCleanup() { + is(GlobalManager.initialized, false, + "GlobalManager start as not initialized"); + + let extension = ExtensionTestUtils.loadExtension({ + background: function() { + browser.test.notifyPass("background page loaded"); + }, + }); + + yield extension.startup(); + + yield extension.awaitFinish("background page loaded"); + + is(GlobalManager.initialized, true, + "GlobalManager has been initialized once an extension is started"); + + yield extension.unload(); + + is(GlobalManager.initialized, false, + "GlobalManager has been uninitialized once all the webextensions have been stopped"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html new file mode 100644 index 000000000..010769500 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html @@ -0,0 +1,164 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" 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"; + +// Test that storage used by a webextension (through localStorage, +// indexedDB, and browser.storage.local) gets cleaned up when the +// extension is uninstalled. +add_task(function* test_uninstall() { + function 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(); + }; + }; + }); + + let browserStoragePromise = browser.storage.local.set({hello: "world"}); + + Promise.all([idbPromise, browserStoragePromise]).then(() => { + browser.test.sendMessage("finished"); + }); + } + + function 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); + }; + }; + }); + + let browserStoragePromise = browser.storage.local.get("hello").then(result => { + return (Object.keys(result).length == 1 && result.hello == "world"); + }); + + Promise.all([idbPromise, browserStoragePromise]) + .then(([matchIDB, matchBrowserStorage]) => { + let result = {matchLocalStorage, matchIDB, matchBrowserStorage}; + browser.test.sendMessage("results", result); + }); + } + + const ID = "storage.cleanup@tests.mozilla.org"; + + // Use a test-only pref to leave the addonid->uuid mapping around after + // uninstall so that we can re-attach to the same storage. Also set + // the pref to prevent cleaning up storage on uninstall so we can test + // that the "keep uuid" logic works correctly. Do the storage flag in + // a separate prefEnv so we can pop it below, leaving the uuid flag set. + yield SpecialPowers.pushPrefEnv({ + set: [["extensions.webextensions.keepUuidOnUninstall", true]], + }); + yield SpecialPowers.pushPrefEnv({ + set: [["extensions.webextensions.keepStorageOnUninstall", true]], + }); + + let extension = ExtensionTestUtils.loadExtension({ + background: writeData, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["storage"], + }, + useAddonManager: "temporary", + }); + + yield extension.startup(); + yield extension.awaitMessage("finished"); + yield 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 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! + yield SpecialPowers.popPrefEnv(); + extension = ExtensionTestUtils.loadExtension({ + background: readData, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["storage"], + }, + useAddonManager: "temporary", + }); + + yield extension.startup(); + let results = yield extension.awaitMessage("results"); + is(results.matchLocalStorage, true, "localStorage data is still present"); + is(results.matchIDB, true, "indexedDB data is still present"); + is(results.matchBrowserStorage, true, "browser.storage.local data is still present"); + + yield extension.unload(); + + // Read again. This time, our data should be gone. + extension = ExtensionTestUtils.loadExtension({ + background: readData, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["storage"], + }, + useAddonManager: "temporary", + }); + + yield extension.startup(); + results = yield extension.awaitMessage("results"); + is(results.matchLocalStorage, false, "localStorage data was cleared"); + is(results.matchIDB, false, "indexedDB data was cleared"); + is(results.matchBrowserStorage, false, "browser.storage.local data was cleared"); + yield extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_trustworthy_origin.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_trustworthy_origin.html new file mode 100644 index 000000000..573c08806 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_trustworthy_origin.html @@ -0,0 +1,53 @@ +<!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/SpawnTask.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="chrome_head.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +/** + * This test is asserting that moz-extension: URLs are recognized as trustworthy local origins + */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "gContentSecurityManager", + "@mozilla.org/contentsecuritymanager;1", + "nsIContentSecurityManager"); + +add_task(function* () { + function background() { + browser.test.sendMessage("ready", browser.runtime.getURL("/test.html")); + } + + let extensionData = { + background, + files: { + "test.html": `<html><head></head><body></body></html>`, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + let url = yield extension.awaitMessage("ready"); + + let uri = NetUtil.newURI(url); + let principal = Services.scriptSecurityManager.getCodebasePrincipal(uri); + is(gContentSecurityManager.isOriginPotentiallyTrustworthy(principal), true); + + yield extension.unload(); +}); +</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 000000000..768eb31fd --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html @@ -0,0 +1,83 @@ +<!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/SpawnTask.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="chrome_head.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(function* webnav_unresolved_uri_on_expected_URI_scheme() { + function background() { + let checkURLs; + + browser.webNavigation.onCompleted.addListener(async msg => { + if (checkURLs.length > 0) { + 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); + + yield extension.startup(); + + let checkURLs = [ + "resource://gre/modules/Services.jsm", + "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js", + "about:mozilla", + ]; + + let tabURL = yield extension.awaitMessage("ready"); + checkURLs.push(tabURL); + + extension.sendMessage("checkURLs", checkURLs); + + for (let url of checkURLs) { + window.open(url); + yield extension.awaitMessage("next"); + } + + yield 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 000000000..a13c4d475 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="chrome_head.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"; + +Cu.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(`recieved ${name}`); + } + + for (let name of eventNames) { + browser.webRequest[name].addListener( + listener.bind(null, name), + {urls: ["https://example.com/*"]} + ); + } + }, +}; + +add_task(function* test_webRequest_chromeworker_events() { + let extension = ExtensionTestUtils.loadExtension(testExtension); + yield extension.startup(); + yield new Promise(resolve => { + let worker = new ChromeWorker("webrequest_chromeworker.js"); + worker.onmessage = event => { + ok("chrome worker fetch finished"); + resolve(); + }; + worker.postMessage("go"); + }); + yield extension.unload(); +}); + +add_task(function* test_webRequest_chromepage_events() { + let extension = ExtensionTestUtils.loadExtension(testExtension); + yield extension.startup(); + yield new Promise(resolve => { + fetch("https://example.com/example.txt").then(() => { + ok("test page loaded"); + resolve(); + }); + }); + yield extension.unload(); +}); + +add_task(function* test_webRequest_jsm_events() { + let extension = ExtensionTestUtils.loadExtension(testExtension); + yield extension.startup(); + yield testFetch("https://example.com/example.txt").then(() => { + ok("fetch page loaded"); + }); + yield testXHR("https://example.com/example.txt").then(() => { + ok("xhr page loaded"); + }); + yield extension.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 000000000..29a148063 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html @@ -0,0 +1,61 @@ +<!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/SpawnTask.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="chrome_head.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +/* global OS */ + +Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/AppConstants.jsm"); + +// Test that the default paths searched for native host manifests +// are the ones we expect. +add_task(function* test_default_paths() { + let expectUser, expectGlobal; + switch (AppConstants.platform) { + case "macosx": { + expectUser = OS.Path.join(OS.Constants.Path.homeDir, + "Library/Application Support/Mozilla/NativeMessagingHosts"); + expectGlobal = "/Library/Application Support/Mozilla/NativeMessagingHosts"; + + break; + } + + case "linux": { + expectUser = OS.Path.join(OS.Constants.Path.homeDir, ".mozilla/native-messaging-hosts"); + + const libdir = AppConstants.HAVE_USR_LIB64_DIR ? "lib64" : "lib"; + expectGlobal = OS.Path.join("/usr", libdir, "mozilla/native-messaging-hosts"); + 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("XREUserNativeMessaging", Ci.nsIFile).path; + is(userDir, expectUser, "user-specific native messaging directory is correct"); + + let globalDir = Services.dirsvc.get("XRESysNativeMessaging", 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_clipboard.html b/toolkit/components/extensions/test/mochitest/test_clipboard.html new file mode 100644 index 000000000..900ee5f10 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_clipboard.html @@ -0,0 +1,140 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>clipboard permission test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/SpawnTask.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"; + +function doCopy(txt) { + let field = document.createElement("textarea"); + document.body.appendChild(field); + field.value = txt; + field.select(); + return document.execCommand("copy"); +} + +add_task(function* no_permission_deny_copy() { + function backgroundScript() { + browser.test.assertEq(false, doCopy("whatever"), + "copy should be denied without permission"); + browser.test.sendMessage("ready"); + } + let extensionData = { + background: `${doCopy};(${backgroundScript})();`, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + yield extension.awaitMessage("ready"); + + yield extension.unload(); +}); + +/** Selecting text in a bg page is not possible, skip test until it's fixed. +add_task(function* with_permission_allow_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: `${doCopy};(${backgroundScript})();`, + manifest: { + permissions: [ + "clipboardWrite", + ], + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + yield extension.awaitMessage("ready"); + + const DUMMY_STR = "dummy string to copy"; + yield new Promise(resolve => { + SimpleTest.waitForClipboard(DUMMY_STR, () => { + extension.sendMessage(DUMMY_STR); + }, resolve, resolve); + }); + + yield extension.unload(); +}); */ + +add_task(function* content_script_no_permission_deny_copy() { + function contentScript() { + browser.test.assertEq(false, doCopy("whatever"), + "copy should be denied without permission"); + browser.test.sendMessage("ready"); + } + let extensionData = { + manifest: { + content_scripts: [{ + js: ["contentscript.js"], + matches: ["http://mochi.test/*/file_sample.html"], + }], + }, + files: { + "contentscript.js": `${doCopy};(${contentScript})();`, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + let win = window.open("file_sample.html"); + yield extension.awaitMessage("ready"); + win.close(); + + yield extension.unload(); +}); + +add_task(function* content_script_with_permission_allow_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: ["contentscript.js"], + matches: ["http://mochi.test/*/file_sample.html"], + }], + permissions: [ + "clipboardWrite", + ], + }, + files: { + "contentscript.js": `${doCopy};(${contentScript})();`, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + let win = window.open("file_sample.html"); + yield extension.awaitMessage("ready"); + + const DUMMY_STR = "dummy string to copy in content script"; + yield new Promise(resolve => { + SimpleTest.waitForClipboard(DUMMY_STR, () => { + extension.sendMessage(DUMMY_STR); + }, resolve, resolve); + }); + + win.close(); + + yield extension.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 000000000..0f617c37e --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js @@ -0,0 +1,158 @@ +/* -*- 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", + // If you want to add a new powerful test API, please see bug 1287233. + "test.assertEq", + "test.assertFalse", + "test.assertRejects", + "test.assertThrows", + "test.assertTrue", + "test.fail", + "test.log", + "test.notifyFail", + "test.notifyPass", + "test.onMessage", + "test.sendMessage", + "test.succeed", +]; + +let expectedContentApis = [ + ...expectedCommonApis, + ...expectedContentApisTargetSpecific, +]; + +let expectedBackgroundApis = [ + ...expectedCommonApis, + ...expectedBackgroundApisTargetSpecific, + "extension.ViewType", + "extension.getBackgroundPage", + "extension.getViews", + "extension.isAllowedFileSchemeAccess", + "extension.isAllowedIncognitoAccess", + // Note: extensionTypes is not visible in Chrome. + "extensionTypes.ImageFormat", + "extensionTypes.RunAt", + "management.ExtensionDisabledReason", + "management.ExtensionInstallType", + "management.ExtensionType", + "management.getSelf", + "management.uninstallSelf", + "runtime.getBackgroundPage", + "runtime.getBrowserInfo", + "runtime.getPlatformInfo", + "runtime.onInstalled", + "runtime.onStartup", + "runtime.onUpdateAvailable", + "runtime.openOptionsPage", + "runtime.reload", + "runtime.setUninstallURL", +]; + +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(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); + yield extension.startup(); + + let win = window.open("file_sample.html"); + let actualApis = yield extension.awaitMessage("allApis"); + win.close(); + let expectedApis = generateExpectations(expectedContentApis); + isDeeply(actualApis, expectedApis, "content script APIs"); + + yield extension.unload(); +}); + +add_task(function* test_enumerate_background_script_apis() { + let extensionData = { + background: sendAllApis, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + let actualApis = yield extension.awaitMessage("allApis"); + let expectedApis = generateExpectations(expectedBackgroundApis); + isDeeply(actualApis, expectedApis, "background script APIs"); + + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_api_injection.html b/toolkit/components/extensions/test/mochitest/test_ext_background_api_injection.html new file mode 100644 index 000000000..f43a59f81 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_background_api_injection.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for privilege escalation into content pages</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +add_task(function* testBackgroundWindow() { + let extension = ExtensionTestUtils.loadExtension({ + background: function() { + const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; + + browser.test.log("background script executed"); + window.location = `${BASE}/file_privilege_escalation.html`; + }, + }); + + let awaitConsole = new Promise(resolve => { + let chromeScript = SpecialPowers.loadChromeScript( + SimpleTest.getTestFileURL("file_ext_test_api_injection.js")); + + chromeScript.addMessageListener("console-message", resolve); + }); + + yield extension.startup(); + + let message = yield awaitConsole; + + ok(message.message.includes("WebExt Privilege Escalation: typeof(browser) = undefined"), + "Document does not have `browser` APIs."); + + yield 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 000000000..bff7190cb --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for background page canvas rendering</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +add_task(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 = { + background, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + yield extension.startup(); + yield extension.awaitFinish("background-canvas"); + yield extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_generated_url.html b/toolkit/components/extensions/test/mochitest/test_ext_background_generated_url.html new file mode 100644 index 000000000..f4fcf3d34 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_background_generated_url.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test _generated_background_page.html</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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> +"use strict"; + +add_task(function* test_url_of_generated_background_page() { + function backgroundScript() { + const EXPECTED_URL = browser.runtime.getURL("/_generated_background_page.html"); + browser.test.assertEq(EXPECTED_URL, location.href); + browser.test.sendMessage("script done", EXPECTED_URL); + } + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + background: { + scripts: ["bg.js"], + }, + web_accessible_resources: ["_generated_background_page.html"], + }, + files: { + "bg.js": backgroundScript, + }, + }); + + yield extension.startup(); + const EXPECTED_URL = yield extension.awaitMessage("script done"); + + let win = window.open(EXPECTED_URL); + ok(win, "Should open new tab at URL: " + EXPECTED_URL); + yield extension.awaitMessage("script done"); + win.close(); + + yield extension.unload(); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_teardown.html b/toolkit/components/extensions/test/mochitest/test_ext_background_teardown.html new file mode 100644 index 000000000..bb6b2e970 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_background_teardown.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for background script teardown</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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> +"use strict"; + +add_task(function* test_background_reload_and_unload() { + function background() { + browser.test.onMessage.addListener(msg => { + browser.test.assertEq("reload-background", msg); + location.reload(); + }); + browser.test.sendMessage("background-url", location.href); + } + + let chromeScript = SpecialPowers.loadChromeScript( + SimpleTest.getTestFileURL("file_teardown_test.js")); + yield chromeScript.promiseOneMessage("chromescript-startup"); + + let extensionData = { + background, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + function* getContextEvents() { + chromeScript.sendAsyncMessage("get-context-events"); + let contextEvents = yield chromeScript.promiseOneMessage("context-events"); + return contextEvents.filter(event => event.extensionId == extension.id); + } + yield extension.startup(); + let backgroundUrl = yield extension.awaitMessage("background-url"); + + let contextEvents = yield* getContextEvents(); + is(contextEvents.length, 1, + "ExtensionContext state change after loading an extension"); + is(contextEvents[0].eventType, "load"); + is(contextEvents[0].url, backgroundUrl, + "The ExtensionContext should be the background page"); + + extension.sendMessage("reload-background"); + yield extension.awaitMessage("background-url"); + + contextEvents = yield* getContextEvents(); + is(contextEvents.length, 2, + "ExtensionContext state changes after reloading the background page"); + is(contextEvents[0].eventType, "unload", + "Unload ExtensionContext of background page"); + is(contextEvents[0].url, backgroundUrl, "ExtensionContext URL = background"); + is(contextEvents[1].eventType, "load", + "Create new ExtensionContext for background page"); + is(contextEvents[1].url, backgroundUrl, "ExtensionContext URL = background"); + yield extension.unload(); + + contextEvents = yield* getContextEvents(); + is(contextEvents.length, 1, + "ExtensionContext state change after unloading the extension"); + is(contextEvents[0].eventType, "unload", + "Unload ExtensionContext for background page after extension unloads"); + is(contextEvents[0].url, backgroundUrl, "ExtensionContext URL = background"); + + chromeScript.sendAsyncMessage("cleanup"); + chromeScript.destroy(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_content_security_policy.html b/toolkit/components/extensions/test/mochitest/test_ext_content_security_policy.html new file mode 100644 index 000000000..a36f29563 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_content_security_policy.html @@ -0,0 +1,162 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension CSP test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +/** + * Tests that content security policies for an add-on are actually applied to * + * documents that belong to it. This tests both the base policies and add-on + * specific policies, and ensures that the parsed policies applied to the + * document's principal match what was specified in the policy string. + * + * @param {object} [customCSP] + */ +function* testPolicy(customCSP = null) { + let baseURL; + + let baseCSP = { + "object-src": ["blob:", "filesystem:", "https://*", "moz-extension:", "'self'"], + "script-src": ["'unsafe-eval'", "'unsafe-inline'", "blob:", "filesystem:", "https://*", "moz-extension:", "'self'"], + }; + + let addonCSP = { + "object-src": ["'self'"], + "script-src": ["'self'"], + }; + + let content_security_policy = null; + + if (customCSP) { + for (let key of Object.keys(customCSP)) { + addonCSP[key] = customCSP[key].split(/\s+/); + } + + content_security_policy = Object.keys(customCSP) + .map(key => `${key} ${customCSP[key]}`) + .join("; "); + } + + + function filterSelf(sources) { + return sources.map(src => src == "'self'" ? baseURL : src); + } + + function checkSource(name, policy, expected) { + is(JSON.stringify(policy[name].sort()), + JSON.stringify(filterSelf(expected[name]).sort()), + `Expected value for ${name}`); + } + + function checkCSP(csp, location) { + let policies = csp["csp-policies"]; + + info(`Base policy for ${location}`); + + is(policies[0]["report-only"], false, "Policy is not report-only"); + checkSource("object-src", policies[0], baseCSP); + checkSource("script-src", policies[0], baseCSP); + + info(`Add-on policy for ${location}`); + + is(policies[1]["report-only"], false, "Policy is not report-only"); + checkSource("object-src", policies[1], addonCSP); + checkSource("script-src", policies[1], addonCSP); + } + + + function getCSP(window) { + let {cspJSON} = SpecialPowers.Cu.getObjectPrincipal(window); + return JSON.parse(cspJSON); + } + + function background(getCSPFn) { + browser.test.sendMessage("base-url", browser.extension.getURL("").replace(/\/$/, "")); + + browser.test.sendMessage("background-csp", getCSPFn(window)); + } + + function tabScript(getCSPFn) { + browser.test.sendMessage("tab-csp", getCSPFn(window)); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${background})(${getCSP})`, + + files: { + "tab.html": `<html><head><meta charset="utf-8"> + <script src="tab.js"></${"script"}></head></html>`, + + "tab.js": `(${tabScript})(${getCSP})`, + + "content.html": `<html><head><meta charset="utf-8"></head></html>`, + }, + + manifest: { + content_security_policy, + + web_accessible_resources: ["content.html", "tab.html"], + }, + }); + + + info(`Testing CSP for policy: ${content_security_policy}`); + + yield extension.startup(); + + baseURL = yield extension.awaitMessage("base-url"); + + + let win1 = window.open(`${baseURL}/tab.html`); + + let frame = document.createElement("iframe"); + frame.src = `${baseURL}/content.html`; + document.body.appendChild(frame); + + yield new Promise(resolve => { + frame.onload = resolve; + }); + + + let backgroundCSP = yield extension.awaitMessage("background-csp"); + checkCSP(backgroundCSP, "background page"); + + let tabCSP = yield extension.awaitMessage("tab-csp"); + checkCSP(tabCSP, "tab page"); + + let contentCSP = getCSP(frame.contentWindow); + checkCSP(contentCSP, "content frame"); + + + win1.close(); + frame.remove(); + + yield extension.unload(); +} + +add_task(function* testCSP() { + yield testPolicy(null); + + let hash = "'sha256-NjZhMDQ1YjQ1MjEwMmM1OWQ4NDBlYzA5N2Q1OWQ5NDY3ZTEzYTNmMzRmNjQ5NGU1MzlmZmQzMmMxYmIzNWYxOCAgLQo='"; + + yield testPolicy({ + "object-src": "'self' https://*.example.com", + "script-src": `'self' https://*.example.com 'unsafe-eval' ${hash}`, + }); + + yield testPolicy({ + "object-src": "'none'", + "script-src": `'self'`, + }); +}); +</script> +</body> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript.html new file mode 100644 index 000000000..39f1bfabd --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript.html @@ -0,0 +1,116 @@ +<!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/SpawnTask.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"; + +add_task(function* test_contentscript() { + function background() { + browser.runtime.onMessage.addListener(([msg, expectedStates, readyState], sender) => { + if (msg == "chrome-namespace-ok") { + browser.test.sendMessage(msg); + return; + } + + browser.test.assertEq("script-run", msg, "message type is correct"); + browser.test.assertTrue(expectedStates.includes(readyState), + `readyState "${readyState}" is one of [${expectedStates}]`); + browser.test.sendMessage("script-run-" + expectedStates[0]); + }); + } + + function contentScriptStart() { + browser.runtime.sendMessage(["script-run", ["loading"], document.readyState]); + } + function contentScriptEnd() { + browser.runtime.sendMessage(["script-run", ["interactive", "complete"], document.readyState]); + } + function contentScriptIdle() { + browser.runtime.sendMessage(["script-run", ["complete"], document.readyState]); + } + + function contentScript() { + let manifest = browser.runtime.getManifest(); + void manifest.applications.gecko.id; + chrome.runtime.sendMessage(["chrome-namespace-ok"]); + } + + let extensionData = { + manifest: { + applications: {gecko: {id: "contentscript@tests.mozilla.org"}}, + content_scripts: [ + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script_start.js"], + "run_at": "document_start", + }, + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script_end.js"], + "run_at": "document_end", + }, + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script_idle.js"], + "run_at": "document_idle", + }, + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_idle", + }, + ], + }, + background, + + files: { + "content_script_start.js": contentScriptStart, + "content_script_end.js": contentScriptEnd, + "content_script_idle.js": contentScriptIdle, + "content_script.js": contentScript, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + let loadingCount = 0; + let interactiveCount = 0; + let completeCount = 0; + extension.onMessage("script-run-loading", () => { loadingCount++; }); + extension.onMessage("script-run-interactive", () => { interactiveCount++; }); + + let completePromise = new Promise(resolve => { + extension.onMessage("script-run-complete", () => { completeCount++; resolve(); }); + }); + + let chromeNamespacePromise = extension.awaitMessage("chrome-namespace-ok"); + + yield extension.startup(); + + let win = window.open("file_sample.html"); + + yield Promise.all([waitForLoad(win), completePromise, chromeNamespacePromise]); + info("test page loaded"); + + win.close(); + + is(loadingCount, 1, "document_start script ran exactly once"); + is(interactiveCount, 1, "document_end script ran exactly once"); + is(completeCount, 1, "document_idle script ran exactly once"); + + yield extension.unload(); + info("extension unloaded"); +}); +</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 000000000..3766678e7 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html @@ -0,0 +1,117 @@ +<!doctype html> +<html> +<head> + <title>Test content script match_about_blank option</title> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" 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(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}); + yield extension.startup(); + + let count = 0; + extension.onMessage("script", script => { + info(`script ran: ${script}`); + count++; + }); + + let win = window.open("http://example.com/" + PATH); + yield 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); + yield 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, "transparent", "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(); + + yield extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_api_injection.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_api_injection.html new file mode 100644 index 000000000..abf3d349f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_api_injection.html @@ -0,0 +1,88 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for privilege escalation into iframe with content script APIs</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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> + +<!-- WORKAROUND: this textarea hack is used to contain the html page source without escaping it --> +<textarea id="test-asset"> + <!DOCTYPE HTML> + <html> + <head> + <meta charset="utf-8"> + <script type="text/javascript" src="./content_script_iframe.js"> + </script> + </head> + </html> +</textarea> + +<script type="text/javascript"> +"use strict"; + +add_task(function* test_contentscript_api_injection() { + function contentScript() { + let iframe = document.createElement("iframe"); + iframe.setAttribute("src", browser.runtime.getURL("content_script_iframe.html")); + document.body.appendChild(iframe); + } + + function contentScriptIframe() { + const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; + window.location = `${BASE}/file_privilege_escalation.html`; + } + + let extensionData = { + manifest: { + content_scripts: [ + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_idle", + }, + ], + "web_accessible_resources": [ + "content_script_iframe.html", + ], + }, + + files: { + "content_script.js": contentScript, + "content_script_iframe.js": contentScriptIframe, + "content_script_iframe.html": document.querySelector("#test-asset").textContent, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + let awaitConsole = new Promise(resolve => { + let chromeScript = SpecialPowers.loadChromeScript( + SimpleTest.getTestFileURL("file_ext_test_api_injection.js")); + + chromeScript.addMessageListener("console-message", resolve); + }); + + yield extension.startup(); + info("extension loaded"); + + let win = window.open("file_sample.html"); + + let message = yield awaitConsole; + + ok(message.message.includes("WebExt Privilege Escalation: typeof(browser) = undefined"), + "Document does not have `browser` APIs."); + + win.close(); + + yield extension.unload(); + info("extension unloaded"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_async_loading.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_async_loading.html new file mode 100644 index 000000000..d78f7ce02 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_async_loading.html @@ -0,0 +1,54 @@ +<!doctype html> +<html> +<head> + <title>Test content script async loading</title> + <script src="/tests/SimpleTest/SpawnTask.js"></script> + <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> +<script> +"use strict"; + +add_task(function* test_async_loading() { + const adder = `(function add(a = 1) { this.count += a; })();\n`; + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [{ + matches: ["https://example.org/"], + js: ["first.js", "second.js"], + }], + }, + files: { + "first.js": ` + this.count = 0; + ${adder.repeat(50000)}; // 2Mb + browser.test.assertEq(this.count, 50000, "A 50k line script"); + + this.order = (this.order || 0) + 1; + browser.test.sendMessage("first", this.order); + `, + "second.js": ` + this.order = (this.order || 0) + 1; + browser.test.sendMessage("second", this.order); + `, + }, + }); + + yield extension.startup(); + const win = window.open("https://example.org/"); + + const [first, second] = yield Promise.all([ + extension.awaitMessage("first"), + extension.awaitMessage("second"), + ]); + + is(first, 1, "first.js finished execution first."); + is(second, 2, "second.js finished execution second."); + + yield extension.unload(); + win.close(); +}); + +</script> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html new file mode 100644 index 000000000..97b1645dd --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script contexts</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +/* eslint-disable mozilla/balanced-listeners */ + +add_task(function* test_contentscript_context() { + function contentScript() { + browser.test.sendMessage("content-script-ready"); + + window.addEventListener("pagehide", () => { + browser.test.sendMessage("content-script-hide"); + }, true); + window.addEventListener("pageshow", () => { + browser.test.sendMessage("content-script-show"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [{ + "matches": ["http://example.com/"], + "js": ["content_script.js"], + "run_at": "document_start", + }], + }, + + files: { + "content_script.js": contentScript, + }, + }); + + yield extension.startup(); + + let win = window.open("http://example.com/"); + yield extension.awaitMessage("content-script-ready"); + yield extension.awaitMessage("content-script-show"); + + // Get the content script context and check that it points to the correct window. + + let {DocumentManager} = SpecialPowers.Cu.import("resource://gre/modules/ExtensionContent.jsm", {}); + let context = DocumentManager.getContentScriptContext(extension, win); + ok(context != null, "Got content script context"); + + is(SpecialPowers.unwrap(context.contentWindow), win, "Context's contentWindow property is correct"); + + // Navigate so that the content page is hidden in the bfcache. + + win.location = "http://example.org/"; + yield extension.awaitMessage("content-script-hide"); + + is(context.contentWindow, null, "Context's contentWindow property is null"); + + // Navigate back so the content page is resurrected from the bfcache. + + SpecialPowers.wrap(win).history.back(); + yield extension.awaitMessage("content-script-show"); + + is(SpecialPowers.unwrap(context.contentWindow), win, "Context's contentWindow property is correct"); + + win.close(); + + yield extension.awaitMessage("content-script-hide"); + + yield extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_create_iframe.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_create_iframe.html new file mode 100644 index 000000000..8aac3e213 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_create_iframe.html @@ -0,0 +1,165 @@ +<!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/SpawnTask.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> + +<!-- WORKAROUND: this textarea hack is used to contain the html page source without escaping it --> +<textarea id="test-asset"> + <!DOCTYPE HTML> + <html> + <head> + <meta charset="utf-8"> + <script type="text/javascript" src="content_script_iframe.js"></script> + </head> + </html> +</textarea> + +<script type="text/javascript"> +"use strict"; + +add_task(function* test_contentscript_create_iframe() { + function background() { + browser.runtime.onMessage.addListener((msg, sender) => { + let {name, availableAPIs, manifest, testGetManifest} = msg; + let hasExtTabsAPI = availableAPIs.indexOf("tabs") > 0; + let hasExtWindowsAPI = availableAPIs.indexOf("windows") > 0; + + browser.test.assertFalse(hasExtTabsAPI, "the created iframe should not be able to use privileged APIs (tabs)"); + browser.test.assertFalse(hasExtWindowsAPI, "the created iframe should not be able to use privileged APIs (windows)"); + + let {applications: {gecko: {id: expectedManifestGeckoId}}} = chrome.runtime.getManifest(); + let {applications: {gecko: {id: actualManifestGeckoId}}} = manifest; + + browser.test.assertEq(actualManifestGeckoId, expectedManifestGeckoId, + "the add-on manifest should be accessible from the created iframe" + ); + + let {applications: {gecko: {id: testGetManifestGeckoId}}} = testGetManifest; + + browser.test.assertEq(testGetManifestGeckoId, expectedManifestGeckoId, + "GET_MANIFEST() returns manifest data before extension unload" + ); + + browser.test.sendMessage(name); + }); + } + + function contentScript() { + let iframe = document.createElement("iframe"); + iframe.setAttribute("src", browser.runtime.getURL("content_script_iframe.html")); + document.body.appendChild(iframe); + } + + function contentScriptIframe() { + window.GET_MANIFEST = browser.runtime.getManifest.bind(null); + + window.testGetManifestException = () => { + try { + window.GET_MANIFEST(); + } catch (exception) { + return String(exception); + } + }; + + let testGetManifest = window.GET_MANIFEST(); + + let manifest = browser.runtime.getManifest(); + let availableAPIs = Object.keys(browser); + + browser.runtime.sendMessage({ + name: "content-script-iframe-loaded", + availableAPIs, + manifest, + testGetManifest, + }); + } + + const ID = "contentscript@tests.mozilla.org"; + let extensionData = { + manifest: { + applications: {gecko: {id: ID}}, + content_scripts: [ + { + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_idle", + }, + ], + web_accessible_resources: [ + "content_script_iframe.html", + ], + }, + + background, + + files: { + "content_script.js": contentScript, + "content_script_iframe.html": document.querySelector("#test-asset").textContent, + "content_script_iframe.js": contentScriptIframe, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + let contentScriptIframeCreatedPromise = new Promise(resolve => { + extension.onMessage("content-script-iframe-loaded", () => { resolve(); }); + }); + + yield extension.startup(); + info("extension loaded"); + + let win = window.open("file_sample.html"); + + yield Promise.all([waitForLoad(win), contentScriptIframeCreatedPromise]); + info("content script privileged iframe loaded and executed"); + + info("testing APIs availability once the extension is unloaded..."); + + let iframeWindow = SpecialPowers.wrap(win)[0]; + + ok(iframeWindow, "content script enabled iframe found"); + ok(/content_script_iframe\.html$/.test(iframeWindow.location), "the found iframe has the expected URL"); + + yield extension.unload(); + info("extension unloaded"); + + info("test content script APIs not accessible from the frame once the extension is unloaded"); + + let ww = SpecialPowers.Cu.waiveXrays(iframeWindow); + let isDeadWrapper = SpecialPowers.Cu.isDeadWrapper(ww.browser); + ok(!isDeadWrapper, "the API object should not be a dead object"); + + let manifest; + let manifestException; + + try { + manifest = ww.browser.runtime.getManifest(); + } catch (e) { + manifestException = e; + } + + ok(!manifest, "manifest should be undefined"); + + is(String(manifestException), "TypeError: ww.browser.runtime is undefined", + "expected exception received"); + + let getManifestException = ww.testGetManifestException(); + + is(getManifestException, "TypeError: can't access dead object", + "expected exception received"); + + win.close(); + + info("done"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_css.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_css.html new file mode 100644 index 000000000..5630a1d68 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_css.html @@ -0,0 +1,48 @@ +<!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/SpawnTask.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"; + +add_task(function* test_content_script_css() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "css": ["content.css"], + }], + }, + + files: { + "content.css": "body { max-width: 42px; }", + }, + }); + + yield extension.startup(); + + let win = window.open("file_sample.html"); + yield waitForLoad(win); + + let style = win.getComputedStyle(win.document.body); + is(style.maxWidth, "42px", "Stylesheet correctly applied"); + + yield extension.unload(); + + style = win.getComputedStyle(win.document.body); + is(style.maxWidth, "none", "Stylesheet correctly removed"); + + win.close(); +}); +</script> + +</body> +</html> 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 000000000..137a3cda4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Sandbox metadata on WebExtensions ContentScripts</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +add_task(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); + + yield extension.startup(); + + let win = window.open("file_sample.html"); + + let innerWindowID = SpecialPowers.wrap(win) + .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor) + .getInterface(SpecialPowers.Ci.nsIDOMWindowUtils) + .currentInnerWindowID; + + yield extension.awaitFinish("contentScript.executed"); + + const {ExtensionContent} = SpecialPowers.Cu.import( + "resource://gre/modules/ExtensionContent.jsm", {} + ); + + let res = ExtensionContent.getContentScriptGlobalsForWindow(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"); + + yield extension.unload(); + info("extension unloaded"); + + res = ExtensionContent.getContentScriptGlobalsForWindow(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_exporthelpers.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_exporthelpers.html new file mode 100644 index 000000000..f3414901d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_exporthelpers.html @@ -0,0 +1,95 @@ +<!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/SpawnTask.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> +"use strict"; + +add_task(function* test_contentscript_exportHelpers() { + function contentScript() { + browser.test.assertTrue(typeof cloneInto === "function"); + browser.test.assertTrue(typeof createObjectIn === "function"); + browser.test.assertTrue(typeof exportFunction === "function"); + + /* globals exportFunction, precisePi, reportPi */ + let value = 3.14; + exportFunction(() => value, window, {defineAs: "precisePi"}); + + browser.test.assertEq("undefined", typeof precisePi, + "exportFunction should export to the page's scope only"); + + browser.test.assertEq("undefined", typeof window.precisePi, + "exportFunction should export to the page's scope only"); + + let results = []; + exportFunction(pi => results.push(pi), window, {defineAs: "reportPi"}); + + let s = document.createElement("script"); + s.textContent = `(${function() { + let result1 = "unknown 1"; + let result2 = "unknown 2"; + try { + result1 = precisePi(); + } catch (e) { + result1 = "err:" + e; + } + try { + result2 = window.precisePi(); + } catch (e) { + result2 = "err:" + e; + } + reportPi(result1); + reportPi(result2); + }})();`; + + document.documentElement.appendChild(s); + // Inline script ought to run synchronously. + + browser.test.assertEq(3.14, results[0], + "exportFunction on window should define a global function"); + browser.test.assertEq(3.14, results[1], + "exportFunction on window should export a property to window."); + + browser.test.assertEq(2, results.length, + "Expecting the number of results to match the number of method calls"); + + browser.test.notifyPass("export helper test completed"); + } + + let extensionData = { + manifest: { + content_scripts: [{ + js: ["contentscript.js"], + matches: ["http://mochi.test/*/file_sample.html"], + run_at: "document_start", + }], + }, + + files: { + "contentscript.js": contentScript, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + yield extension.startup(); + + let win = window.open("file_sample.html"); + + yield extension.awaitFinish("export helper test completed"); + win.close(); + + yield extension.unload(); +}); +</script> + +</body> +</html> 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 000000000..a2f38dce6 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script private browsing ID</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +add_task(function* test_contentscript_incognito() { + let extension = ExtensionTestUtils.loadExtension({ + 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"); + } + }, + }, + }); + + yield extension.startup(); + + extension.sendMessage(["open-window", SimpleTest.getTestFileURL("file_sample.html")]); + + yield extension.awaitFinish("cookies"); + + extension.sendMessage(["close-window"]); + yield extension.awaitMessage("done"); + + yield extension.unload(); +}); +</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 000000000..eaf815092 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html @@ -0,0 +1,59 @@ +<!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/SpawnTask.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"; + +add_task(function* test_contentscript() { + function background() { + browser.test.onMessage.addListener(url => { + browser.tabs.create({url}).then(tab => { + return browser.tabs.executeScript(tab.id, {code: "true;"}) + .then(() => { + browser.test.sendMessage("executed", true); + browser.tabs.remove([tab.id]); + }, err => { + browser.test.sendMessage("executed", false); + browser.tabs.remove([tab.id]); + }); + }); + }); + } + + let extensionData = { + manifest: { + permissions: ["<all_urls>"], + }, + background, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + extension.sendMessage("https://example.com"); + let result = yield extension.awaitMessage("executed"); + is(result, true, "Content script can be run in a page without mozAddonManager"); + + yield SpecialPowers.pushPrefEnv({ + set: [["extensions.webapi.testing", true]], + }); + + extension.sendMessage("https://example.com"); + result = yield extension.awaitMessage("executed"); + is(result, false, "Content script cannot be run in a page with mozAddonManager"); + + yield extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_teardown.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_teardown.html new file mode 100644 index 000000000..33a8c4ccc --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_teardown.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script teardown</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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> +"use strict"; + +add_task(function* test_contentscript_reload_and_unload() { + function contentScript() { + browser.test.sendMessage("contentscript-run"); + } + function background() { + let removedTabs = 0; + browser.tabs.onRemoved.addListener(() => { + browser.test.assertEq(1, ++removedTabs, + "Expected only one tab to be removed during the test"); + browser.test.sendMessage("tab-closed"); + }); + } + + let extensionData = { + background, + manifest: { + content_scripts: [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["contentscript.js"], + }], + }, + + files: { + "contentscript.js": contentScript, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + let chromeScript = SpecialPowers.loadChromeScript( + SimpleTest.getTestFileURL("file_teardown_test.js")); + yield chromeScript.promiseOneMessage("chromescript-startup"); + function* getContextEvents() { + chromeScript.sendAsyncMessage("get-context-events"); + let contextEvents = yield chromeScript.promiseOneMessage("context-events"); + return contextEvents.filter(event => event.extensionId == extension.id); + } + + let win = window.open("file_sample.html"); + yield extension.awaitMessage("contentscript-run"); + let tabUrl = win.location.href; + + let contextEvents = yield* getContextEvents(); + is(contextEvents.length, 1, + "ExtensionContext state change after loading a content script"); + is(contextEvents[0].eventType, "load", + "Create ExtensionContext for content script"); + is(contextEvents[0].url, tabUrl, "ExtensionContext URL = page"); + + let promiseReload = extension.awaitMessage("contentscript-run"); + win.location.reload(); + yield promiseReload; + contextEvents = yield* getContextEvents(); + is(contextEvents.length, 2, + "ExtensionContext state changes after reloading a content script"); + is(contextEvents[0].eventType, "unload", "Unload old ExtensionContext"); + is(contextEvents[0].url, tabUrl, "ExtensionContext URL = page"); + is(contextEvents[1].eventType, "load", + "Create new ExtensionContext for content script"); + is(contextEvents[1].url, tabUrl, "ExtensionContext URL = page"); + + let tabClosePromise = extension.awaitMessage("tab-closed"); + win.close(); + yield tabClosePromise; + + contextEvents = yield* getContextEvents(); + is(contextEvents.length, 1, + "ExtensionContext state change after unloading a content script"); + is(contextEvents[0].eventType, "unload", + "Unload ExtensionContext after closing the tab with the content script"); + is(contextEvents[0].url, tabUrl, "ExtensionContext URL = page"); + + chromeScript.sendAsyncMessage("cleanup"); + chromeScript.destroy(); + yield 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 000000000..d414a4e46 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies.html @@ -0,0 +1,234 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +add_task(function* test_cookies() { + 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"); + } + + 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, + session: false, + expirationDate: THE_FUTURE, + storeId: STORE_ID, + }; + + 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(cookies.length, 1, "one cookie found for matching name"); + assertExpected(expected, cookies[0]); + + cookies = await browser.cookies.getAll({domain: "example.org"}); + browser.test.assertEq(cookies.length, 1, "one cookie found for matching domain"); + assertExpected(expected, cookies[0]); + + cookies = await browser.cookies.getAll({domain: "example.net"}); + browser.test.assertEq(cookies.length, 0, "no cookies found for non-matching domain"); + + cookies = await browser.cookies.getAll({secure: false}); + browser.test.assertEq(cookies.length, 1, "one non-secure cookie found"); + assertExpected(expected, cookies[0]); + + cookies = await browser.cookies.getAll({secure: true}); + browser.test.assertEq(cookies.length, 0, "no secure cookies found"); + + cookies = await browser.cookies.getAll({storeId: STORE_ID}); + browser.test.assertEq(cookies.length, 1, "one cookie found for valid storeId"); + assertExpected(expected, cookies[0]); + + cookies = await browser.cookies.getAll({storeId: "invalid_id"}); + browser.test.assertEq(cookies.length, 0, "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}, 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 tab returned for store"); + + { + let privateWindow = await browser.windows.create({incognito: true}); + 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(privateWindow.id); + } + + 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}, 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(cookies.length, 1, "one session cookie found"); + browser.test.assertEq(true, cookies[0].session, "found session cookie"); + + cookies = await browser.cookies.getAll({session: false}); + browser.test.assertEq(cookies.length, 0, "no non-session cookies found"); + + details = await browser.cookies.remove({url: TEST_URL, name: "name1"}); + assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID}, 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(cookies.length, 1, "one secure cookie found"); + browser.test.assertEq(true, cookies[0].secure, "found secure cookie"); + + cookies = await browser.cookies.getAll({secure: false}); + browser.test.assertEq(cookies.length, 0, "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}, 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(cookies.length, 1, "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(cookies.length, 0, "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}, 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}, 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"); + + { + let privateWindow = await browser.windows.create({incognito: true}); + + // Hacky work-around for bugzil.la/1309637 + await new Promise(resolve => setTimeout(resolve, 700)); + + 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"); + + let details = await browser.cookies.remove({url: TEST_URL, name: "store", storeId: STORE_ID}); + assertExpected({url: TEST_URL, name: "store", storeId: STORE_ID}, 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}, details); + + cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID}); + browser.test.assertEq(null, cookie, "deleted the private cookie"); + + await browser.windows.remove(privateWindow.id); + } + + browser.test.notifyPass("cookies"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["cookies", "*://example.org/"], + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("cookies"); + yield 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 000000000..bc4994eec --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html @@ -0,0 +1,93 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="chrome_head.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"; + +add_task(function* setup() { + // make sure userContext is enabled. + return SpecialPowers.pushPrefEnv({"set": [ + ["privacy.userContext.enabled", true], + ]}); +}); + +add_task(function* test_cookie_containers() { + 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"); + } + + 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, + session: false, + expirationDate: THE_FUTURE, + storeId: "firefox-container-1", + }; + + 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, cookies.length, "getAll() with default storeId returns an empty array"); + + 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"}, 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/"], + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("cookies"); + yield 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 000000000..3927d9e94 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="chrome_head.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"; + +add_task(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 domain = ".example.com"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["http://example.com/", "cookies"], + }, + background, + }); + + let cookieSvc = SpecialPowers.Services.cookies; + + let cookie = { + host: domain, + name: "first", + path: "/", + }; + + do { + cookieSvc.add(cookie.host, cookie.path, cookie.name, "one", false, false, false, Date.now() / 1000 + 1); + } while (!cookieSvc.cookieExists(cookie)); + + yield extension.startup(); + yield extension.awaitMessage("change-cookies"); + + cookieSvc.add(cookie.host, cookie.path, cookie.name, "one-again", false, false, false, Date.now() / 1000 + 10); + + yield extension.awaitFinish("cookie-expiry"); + yield extension.unload(); +}); + +</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 000000000..15a62855a --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="chrome_head.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="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(function* init() { + // We need to trigger a cookie eviction in order to test our batch delete + // observer. + SpecialPowers.setIntPref("network.cookie.maxPerHost", 3); + SimpleTest.registerCleanupFunction(() => { + SpecialPowers.clearUserPref("network.cookie.maxPerHost"); + }); +}); + +add_task(function* test_bad_cookie_permissions() { + info("Test non-matching, non-secure domain with non-secure cookie"); + yield 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"); + yield 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"); + yield 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)"); + yield 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"); + yield 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"); + yield testCookies({ + permissions: ["http://example.com/", "cookies"], + url: "https://example.com/", + domain: "example.com", + secure: true, + shouldPass: false, + shouldWrite: false, + }); + + info("Test non-matching domain"); + yield testCookies({ + permissions: ["http://example.com/", "cookies"], + url: "http://example.com/", + domain: "example.net", + secure: false, + shouldPass: false, + shouldWrite: false, + }); + + info("Test invalid scheme"); + yield 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 000000000..31e83188c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="chrome_head.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="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(function* init() { + // We need to trigger a cookie eviction in order to test our batch delete + // observer. + SpecialPowers.setIntPref("network.cookie.maxPerHost", 3); + SimpleTest.registerCleanupFunction(() => { + SpecialPowers.clearUserPref("network.cookie.maxPerHost"); + }); +}); + +add_task(function* test_good_cookie_permissions() { + info("Test matching, non-secure domain with non-secure cookie"); + yield 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"); + yield 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"); + yield 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)"); + yield 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)"); + yield 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)"); + yield 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_exclude_include_globs.html b/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html new file mode 100644 index 000000000..640522b40 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html @@ -0,0 +1,92 @@ +<!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/SpawnTask.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"; + +add_task(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++; + }); + + yield Promise.all([extension.startup(), extension.awaitMessage("running")]); + info("extension loaded"); + + let win = window.open("http://example.org/"); + yield Promise.all([extension.awaitMessage("run-all"), extension.awaitMessage("run-excludes-test1")]); + win.close(); + is(ran, 2); + + win = window.open("http://test1.example.org/"); + yield Promise.all([extension.awaitMessage("run-all"), extension.awaitMessage("run-includes-test1")]); + win.close(); + is(ran, 4); + + yield extension.unload(); + info("extension unloaded"); +}); +</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 000000000..cfafcbad9 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_generate.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for generating WebExtensions</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +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(function* test_background() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + info("load complete"); + let [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]); + is(x, 1, "got correct value from extension"); + info("startup complete"); + extension.sendMessage(10, 20); + yield extension.awaitFinish(); + info("test complete"); + yield extension.unload(); + info("extension unloaded successfully"); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_geturl.html b/toolkit/components/extensions/test/mochitest/test_ext_geturl.html new file mode 100644 index 000000000..6e39c2f5d --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_geturl.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +function background() { + browser.runtime.onMessage.addListener(([url1, url2]) => { + let url3 = browser.runtime.getURL("test_file.html"); + let url4 = browser.extension.getURL("test_file.html"); + + browser.test.assertTrue(url1 !== undefined, "url1 defined"); + + browser.test.assertTrue(url1.startsWith("moz-extension://"), "url1 has correct scheme"); + browser.test.assertTrue(url1.endsWith("test_file.html"), "url1 has correct leaf name"); + + browser.test.assertEq(url1, url2, "url2 matches"); + browser.test.assertEq(url1, url3, "url3 matches"); + browser.test.assertEq(url1, url4, "url4 matches"); + + browser.test.notifyPass("geturl"); + }); +} + +function contentScript() { + let url1 = browser.runtime.getURL("test_file.html"); + let url2 = browser.extension.getURL("test_file.html"); + browser.runtime.sendMessage([url1, url2]); +} + +let extensionData = { + background, + manifest: { + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_idle", + }], + }, + + files: { + "content_script.js": contentScript, + }, +}; + +add_task(function* test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + let win = window.open("file_sample.html"); + + yield Promise.all([waitForLoad(win), extension.awaitFinish("geturl")]); + + win.close(); + + yield extension.unload(); + info("extension unloaded"); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_i18n.html b/toolkit/components/extensions/test/mochitest/test_ext_i18n.html new file mode 100644 index 000000000..1f7330bbb --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_i18n.html @@ -0,0 +1,432 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for WebExtension localization APIs</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +SimpleTest.registerCleanupFunction(() => { SpecialPowers.clearUserPref("intl.accept_languages"); }); +SimpleTest.registerCleanupFunction(() => { SpecialPowers.clearUserPref("general.useragent.locale"); }); + +add_task(function* test_i18n() { + function runTests(assertEq) { + let _ = browser.i18n.getMessage.bind(browser.i18n); + + let url = browser.runtime.getURL("/"); + assertEq(url, `moz-extension://${_("@@extension_id")}/`, "@@extension_id builtin message"); + + assertEq("Foo.", _("Foo"), "Simple message in selected locale."); + + assertEq("(bar)", _("bar"), "Simple message fallback in default locale."); + + assertEq("", _("some-unknown-locale-string"), "Unknown locale string."); + + assertEq("", _("@@unknown_builtin_string"), "Unknown built-in string."); + assertEq("", _("@@bidi_unknown_builtin_string"), "Unknown built-in bidi string."); + + assertEq("Føo.", _("Föo"), "Multi-byte message in selected locale."); + + let substitutions = []; + substitutions[4] = "5"; + substitutions[13] = "14"; + + assertEq("'$0' '14' '' '5' '$$$$' '$'.", _("basic_substitutions", substitutions), + "Basic numeric substitutions"); + + assertEq("'$0' '' 'just a string' '' '$$$$' '$'.", _("basic_substitutions", "just a string"), + "Basic numeric substitutions, with non-array value"); + + let values = _("named_placeholder_substitutions", ["(subst $1 $2)", "(2 $1 $2)"]).split("\n"); + + assertEq("_foo_ (subst $1 $2) _bar_", values[0], "Named and numeric substitution"); + + assertEq("(2 $1 $2)", values[1], "Numeric substitution amid named placeholders"); + + assertEq("$bad name$", values[2], "Named placeholder with invalid key"); + + assertEq("", values[3], "Named placeholder with an invalid value"); + + assertEq("Accepted, but shouldn't break.", values[4], "Named placeholder with a strange content value"); + + assertEq("$foo", values[5], "Non-placeholder token that should be ignored"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "default_locale": "jp", + + content_scripts: [ + {"matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content.js"]}, + ], + }, + + + files: { + "_locales/en_US/messages.json": { + "foo": { + "message": "Foo.", + "description": "foo", + }, + + "föo": { + "message": "Føo.", + "description": "foo", + }, + + "basic_substitutions": { + "message": "'$0' '$14' '$1' '$5' '$$$$$' '$$'.", + "description": "foo", + }, + + "Named_placeholder_substitutions": { + "message": "$Foo$\n$2\n$bad name$\n$bad_value$\n$bad_content_value$\n$foo", + "description": "foo", + "placeholders": { + "foO": { + "content": "_foo_ $1 _bar_", + "description": "foo", + }, + + "bad name": { + "content": "Nope.", + "description": "bad name", + }, + + "bad_value": "Nope.", + + "bad_content_value": { + "content": ["Accepted, but shouldn't break."], + "description": "bad value", + }, + }, + }, + + "broken_placeholders": { + "message": "$broken$", + "description": "broken placeholders", + "placeholders": "foo.", + }, + }, + + "_locales/jp/messages.json": { + "foo": { + "message": "(foo)", + "description": "foo", + }, + + "bar": { + "message": "(bar)", + "description": "bar", + }, + }, + + "content.js": "new " + function(runTestsFn) { + runTestsFn((...args) => { + browser.runtime.sendMessage(["assertEq", ...args]); + }); + + browser.runtime.sendMessage(["content-script-finished"]); + } + `(${runTests})`, + }, + + background: "new " + function(runTestsFn) { + browser.runtime.onMessage.addListener(([msg, ...args]) => { + if (msg == "assertEq") { + browser.test.assertEq(...args); + } else { + browser.test.sendMessage(msg, ...args); + } + }); + + runTestsFn(browser.test.assertEq.bind(browser.test)); + } + `(${runTests})`, + }); + + yield extension.startup(); + + let win = window.open("file_sample.html"); + yield extension.awaitMessage("content-script-finished"); + win.close(); + + yield extension.unload(); +}); + +add_task(function* test_get_accept_languages() { + function background() { + function checkResults(source, results, expected) { + browser.test.assertEq( + expected.length, + results.length, + `got expected number of languages in ${source}`); + results.forEach((lang, index) => { + browser.test.assertEq( + expected[index], + lang, + `got expected language in ${source}`); + }); + } + + let tabId; + + browser.tabs.query({currentWindow: true, active: true}, tabs => { + tabId = tabs[0].id; + browser.test.sendMessage("ready"); + }); + + browser.test.onMessage.addListener(async ([msg, expected]) => { + let contentResults = await browser.tabs.sendMessage(tabId, "get-results"); + let backgroundResults = await browser.i18n.getAcceptLanguages(); + + checkResults("contentScript", contentResults, expected); + checkResults("background", backgroundResults, expected); + + browser.test.sendMessage("done"); + }); + } + + function content() { + browser.runtime.onMessage.addListener((msg, sender, respond) => { + browser.i18n.getAcceptLanguages(respond); + return true; + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "run_at": "document_start", + "js": ["content_script.js"], + }], + }, + + background, + + files: { + "content_script.js": content, + }, + }); + + let win = window.open("file_sample.html"); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + let expectedLangs = ["en-US", "en"]; + extension.sendMessage(["expect-results", expectedLangs]); + yield extension.awaitMessage("done"); + + expectedLangs = ["en-US", "en", "fr-CA", "fr"]; + SpecialPowers.setCharPref("intl.accept_languages", expectedLangs.toString()); + extension.sendMessage(["expect-results", expectedLangs]); + yield extension.awaitMessage("done"); + SpecialPowers.clearUserPref("intl.accept_languages"); + + win.close(); + + yield extension.unload(); +}); + +add_task(function* test_get_ui_language() { + function getResults() { + return { + getUILanguage: browser.i18n.getUILanguage(), + getMessage: browser.i18n.getMessage("@@ui_locale"), + }; + } + + function background(getResultsFn) { + function checkResults(source, results, expected) { + browser.test.assertEq( + expected, + results.getUILanguage, + `Got expected getUILanguage result in ${source}` + ); + browser.test.assertEq( + expected, + results.getMessage, + `Got expected getMessage result in ${source}` + ); + } + + let tabId; + + browser.test.onMessage.addListener(([msg, expected]) => { + browser.tabs.sendMessage(tabId, "get-results", result => { + checkResults("contentScript", result, expected); + checkResults("background", getResultsFn(), expected); + + browser.test.sendMessage("done"); + }); + }); + + browser.tabs.query({currentWindow: true, active: true}, tabs => { + tabId = tabs[0].id; + browser.test.sendMessage("ready"); + }); + } + + function content(getResultsFn) { + browser.runtime.onMessage.addListener((msg, sender, respond) => { + respond(getResultsFn()); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "run_at": "document_start", + "js": ["content_script.js"], + }], + }, + + background: `(${background})(${getResults})`, + + files: { + "content_script.js": `(${content})(${getResults})`, + }, + }); + + let win = window.open("file_sample.html"); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + extension.sendMessage(["expect-results", "en_US"]); + yield extension.awaitMessage("done"); + + SpecialPowers.setCharPref("general.useragent.locale", "he"); + + extension.sendMessage(["expect-results", "he"]); + yield extension.awaitMessage("done"); + + win.close(); + + yield extension.unload(); +}); + + +add_task(function* test_detect_language() { + const af_string = " aam skukuza die naam beteken hy wat skoonvee of hy wat alles onderstebo keer wysig " + + "bosveldkampe boskampe is kleiner afgeleë ruskampe wat oor min fasiliteite beskik daar is geen restaurante " + + "of winkels nie en slegs oornagbesoekers word toegelaat bateleur"; + // String with intermixed French/English text + const fr_en_string = "France is the largest country in Western Europe and the third-largest in Europe as a whole. " + + "A accès aux chiens et aux frontaux qui lui ont été il peut consulter et modifier ses collections et exporter " + + "Cet article concerne le pays européen aujourd’hui appelé République française. Pour d’autres usages du nom France, " + + "Pour une aide rapide et effective, veuiller trouver votre aide dans le menu ci-dessus." + + "Motoring events began soon after the construction of the first successful gasoline-fueled automobiles. The quick brown fox jumped over the lazy dog"; + + function background() { + function checkResult(source, result, expected) { + browser.test.assertEq(expected.isReliable, result.isReliable, "result.confident is true"); + browser.test.assertEq( + expected.languages.length, + result.languages.length, + `result.languages contains the expected number of languages in ${source}`); + expected.languages.forEach((lang, index) => { + browser.test.assertEq( + lang.percentage, + result.languages[index].percentage, + `element ${index} of result.languages array has the expected percentage in ${source}`); + browser.test.assertEq( + lang.language, + result.languages[index].language, + `element ${index} of result.languages array has the expected language in ${source}`); + }); + } + + let tabId; + + browser.tabs.query({currentWindow: true, active: true}, tabs => { + tabId = tabs[0].id; + browser.test.sendMessage("ready"); + }); + + browser.test.onMessage.addListener(async ([msg, expected]) => { + let backgroundResults = await browser.i18n.detectLanguage(msg); + let contentResults = await browser.tabs.sendMessage(tabId, msg); + + checkResult("background", backgroundResults, expected); + checkResult("contentScript", contentResults, expected); + + browser.test.sendMessage("done"); + }); + } + + function content() { + browser.runtime.onMessage.addListener((msg, sender, respond) => { + browser.i18n.detectLanguage(msg, respond); + return true; + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "run_at": "document_start", + "js": ["content_script.js"], + }], + }, + + background, + + files: { + "content_script.js": content, + }, + }); + + let win = window.open("file_sample.html"); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + let expected = { + isReliable: true, + languages: [ + { + language: "fr", + percentage: 67, + }, + { + language: "en", + percentage: 32, + }, + ], + }; + extension.sendMessage([fr_en_string, expected]); + yield extension.awaitMessage("done"); + + expected = { + isReliable: true, + languages: [ + { + language: "af", + percentage: 99, + }, + ], + }; + extension.sendMessage([af_string, expected]); + yield extension.awaitMessage("done"); + + win.close(); + + yield extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_i18n_css.html b/toolkit/components/extensions/test/mochitest/test_ext_i18n_css.html new file mode 100644 index 000000000..7c6a8eeaa --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_i18n_css.html @@ -0,0 +1,116 @@ +<!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/SpawnTask.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"; + +add_task(function* test_i18n_css() { + let extension = ExtensionTestUtils.loadExtension({ + background: function() { + function backgroundFetch(url) { + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + xhr.open("GET", url); + xhr.onload = () => { resolve(xhr.responseText); }; + xhr.onerror = reject; + xhr.send(); + }); + } + + Promise.all([backgroundFetch("foo.css"), backgroundFetch("bar.CsS?x#y"), backgroundFetch("foo.txt")]).then(results => { + browser.test.assertEq("body { max-width: 42px; }", results[0], "CSS file localized"); + browser.test.assertEq("body { max-width: 42px; }", results[1], "CSS file localized"); + + browser.test.assertEq("body { __MSG_foo__; }", results[2], "Text file not localized"); + + browser.test.notifyPass("i18n-css"); + }); + + browser.test.sendMessage("ready", browser.runtime.getURL("foo.css")); + }, + + manifest: { + "web_accessible_resources": ["foo.css", "foo.txt", "locale.css"], + + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "css": ["foo.css"], + }], + + "default_locale": "en", + }, + + files: { + "_locales/en/messages.json": JSON.stringify({ + "foo": { + "message": "max-width: 42px", + "description": "foo", + }, + }), + + "foo.css": "body { __MSG_foo__; }", + "bar.CsS": "body { __MSG_foo__; }", + "foo.txt": "body { __MSG_foo__; }", + "locale.css": '* { content: "__MSG_@@ui_locale__ __MSG_@@bidi_dir__ __MSG_@@bidi_reversed_dir__ __MSG_@@bidi_start_edge__ __MSG_@@bidi_end_edge__" }', + }, + }); + + yield extension.startup(); + let cssURL = yield extension.awaitMessage("ready"); + + function fetch(url) { + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + xhr.open("GET", url); + xhr.onload = () => { resolve(xhr.responseText); }; + xhr.onerror = reject; + xhr.send(); + }); + } + + let css = yield fetch(cssURL); + + is(css, "body { max-width: 42px; }", "CSS file localized in mochitest scope"); + + let win = window.open("file_sample.html"); + yield waitForLoad(win); + + let style = win.getComputedStyle(win.document.body); + is(style.maxWidth, "42px", "stylesheet correctly applied"); + win.close(); + + cssURL = cssURL.replace(/foo.css$/, "locale.css"); + + css = yield fetch(cssURL); + is(css, '* { content: "en_US ltr rtl left right" }', "CSS file localized in mochitest scope"); + + const LOCALE = "general.useragent.locale"; + const DIR = "intl.uidirection.en"; + + // We don't wind up actually switching the chrome registry locale, since we + // don't have a chrome package for Hebrew. So just override it. + SpecialPowers.setCharPref(LOCALE, "he"); + SpecialPowers.setCharPref(DIR, "rtl"); + + css = yield fetch(cssURL); + is(css, '* { content: "he rtl ltr right left" }', "CSS file localized in mochitest scope"); + + SpecialPowers.clearUserPref(LOCALE); + SpecialPowers.clearUserPref(DIR); + + yield extension.awaitFinish("i18n-css"); + yield 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 000000000..675cbb298 --- /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 type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +add_task(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>`, + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("inIncognitoContext"); + yield extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_jsversion.html b/toolkit/components/extensions/test/mochitest/test_ext_jsversion.html new file mode 100644 index 000000000..da0c355e0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_jsversion.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <meta charset="utf-8"> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="chrome_head.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"; + +add_task(function* test_versioned_js() { + // We need to deal with escaping the close script tags. + // May as well consolidate it into one place. + let script = attrs => `<script ${attrs}><\/script>`; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "background": {"page": "background.html"}, + }, + + files: { + "background.html": ` + <meta charset="utf-8"> + ${script('src="background.js" type="application/javascript"')} + ${script('src="background-1.js" type="application/javascript;version=1.8"')} + ${script('src="background-2.js" type="application/javascript;version=latest"')} + ${script('src="background-3.js" type="application/javascript"')} + `, + + "background.js": function() { + window.reportResult = msg => { + browser.test.assertEq( + msg, "background-script-3", + "Expected a message only from the unversioned background script."); + + browser.test.sendMessage("finished"); + }; + }, + + "background-1.js": function() { + window.reportResult("background-script-1"); + }, + "background-2.js": function() { + window.reportResult("background-script-2"); + }, + "background-3.js": function() { + window.reportResult("background-script-3"); + }, + }, + }); + + let messages = [/Versioned JavaScript.*not supported in WebExtension.*developer\.mozilla\.org/, + /Versioned JavaScript.*not supported in WebExtension.*developer\.mozilla\.org/]; + + let waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, messages); + }); + + info("loading extension"); + + yield Promise.all([extension.startup(), + extension.awaitMessage("finished")]); + + info("waiting for console"); + + SimpleTest.endMonitorConsole(); + yield waitForConsole; + + info("unloading extension"); + + yield extension.unload(); + + info("test complete"); +}); + +</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 000000000..ca8db873e --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html @@ -0,0 +1,63 @@ +<!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/SpawnTask.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"; + +add_task(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"); + }, + }); + + yield extension.startup(); + + yield extension.awaitFinish("onchanged-listeners"); + + yield 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 000000000..d1b798cf9 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_notifications.html @@ -0,0 +1,224 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for notifications</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" 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"; + +// 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(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, + }); + yield extension.startup(); + let x = yield extension.awaitMessage("running"); + is(x, "0", "got correct id from notifications.create"); + yield extension.awaitFinish(); + yield extension.unload(); +}); + +add_task(function* test_notification_events() { + async function background() { + let opts = { + type: "basic", + title: "Testing Notification", + message: "Carry on", + }; + + // 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(function() {}); + + // Test onClosed listener. + browser.notifications.onClosed.addListener(id => { + browser.test.sendMessage("closed", id); + browser.test.notifyPass("background test passed"); + }); + + await browser.notifications.create("5", opts); + let id = await browser.notifications.create("5", opts); + browser.test.sendMessage("running", id); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["notifications"], + }, + background, + }); + yield extension.startup(); + let x = yield extension.awaitMessage("closed"); + is(x, "5", "got correct id from onClosed listener"); + x = yield extension.awaitMessage("running"); + is(x, "5", "got correct id from notifications.create"); + yield extension.awaitFinish(); + yield extension.unload(); +}); + +add_task(function* test_notification_clear() { + async function background() { + let opts = { + type: "basic", + title: "Testing Notification", + message: "Carry on", + }; + + browser.notifications.onClosed.addListener(id => { + browser.test.sendMessage("closed", id); + }); + + let id = await browser.notifications.create("99", opts); + + let wasCleared = await browser.notifications.clear(id); + browser.test.sendMessage("cleared", wasCleared); + + browser.test.notifyPass("background test passed"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["notifications"], + }, + background, + }); + yield extension.startup(); + let x = yield extension.awaitMessage("closed"); + is(x, "99", "got correct id from onClosed listener"); + x = yield extension.awaitMessage("cleared"); + is(x, true, "got correct boolean from notifications.clear"); + yield extension.awaitFinish(); + yield extension.unload(); +}); + +add_task(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, + }); + yield extension.startup(); + yield extension.awaitFinish("getAll empty"); + yield extension.unload(); +}); + +add_task(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, + }, + }); + yield extension.startup(); + yield extension.awaitFinish("getAll populated"); + yield extension.unload(); +}); + +add_task(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, + }); + yield extension.startup(); + yield extension.awaitFinish("buttons-unsupported"); + yield extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_permission_xhr.html b/toolkit/components/extensions/test/mochitest/test_ext_permission_xhr.html new file mode 100644 index 000000000..07967d5d0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_permission_xhr.html @@ -0,0 +1,119 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +/* eslint-disable mozilla/balanced-listeners */ + +add_task(function* test_simple() { + async function runTests(cx) { + function xhr(XMLHttpRequest) { + return (url) => { + return new Promise((resolve, reject) => { + let req = new XMLHttpRequest(); + req.open("GET", url); + req.addEventListener("load", resolve); + req.addEventListener("error", reject); + req.send(); + }); + }; + } + + function run(shouldFail, fetch) { + function passListener() { + browser.test.succeed(`${cx}.${fetch.name} pass listener`); + } + + function failListener() { + browser.test.fail(`${cx}.${fetch.name} fail listener`); + } + + /* eslint-disable no-else-return */ + if (shouldFail) { + return fetch("http://example.org/example.txt").then(failListener, passListener); + } else { + return fetch("http://example.com/example.txt").then(passListener, failListener); + } + /* eslint-enable no-else-return */ + } + + try { + await run(true, xhr(XMLHttpRequest)); + await run(false, xhr(XMLHttpRequest)); + await run(true, xhr(window.XMLHttpRequest)); + await run(false, xhr(window.XMLHttpRequest)); + await run(true, fetch); + await run(false, fetch); + await run(true, window.fetch); + await run(false, window.fetch); + } catch (err) { + browser.test.fail(`Error: ${err} :: ${err.stack}`); + browser.test.notifyFail("permission_xhr"); + } + } + + async function background(runTestsFn) { + await runTestsFn("bg"); + browser.test.notifyPass("permission_xhr"); + } + + let extensionData = { + background: `(${background})(${runTests})`, + manifest: { + permissions: ["http://example.com/"], + content_scripts: [{ + "matches": ["http://mochi.test/*/file_permission_xhr.html"], + "js": ["content.js"], + }], + }, + files: { + "content.js": `(${async runTestsFn => { + await runTestsFn("content"); + + window.wrappedJSObject.privilegedFetch = fetch; + window.wrappedJSObject.privilegedXHR = XMLHttpRequest; + + window.addEventListener("message", function rcv({data}) { + switch (data.msg) { + case "test": + break; + + case "assertTrue": + browser.test.assertTrue(data.condition, data.description); + break; + + case "finish": + window.removeEventListener("message", rcv, false); + browser.test.sendMessage("content-script-finished"); + break; + } + }, false); + window.postMessage("test", "*"); + }})(${runTests})`, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + let win = window.open("file_permission_xhr.html"); + yield extension.awaitMessage("content-script-finished"); + win.close(); + + yield extension.awaitFinish("permission_xhr"); + yield extension.unload(); +}); +</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 000000000..60351eaee --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html @@ -0,0 +1,83 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +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(function* test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + let win = window.open("file_sample.html"); + + yield Promise.all([waitForLoad(win), extension.awaitFinish("runtime.connect")]); + + win.close(); + + yield 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 000000000..dce12b21b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html @@ -0,0 +1,103 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +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(function* test_contentscript() { + let extension1 = ExtensionTestUtils.loadExtension(makeExtension()); + let extension2 = ExtensionTestUtils.loadExtension(makeExtension()); + yield Promise.all([extension1.startup(), extension2.startup()]); + + yield extension1.awaitMessage("background-ready"); + yield extension2.awaitMessage("background-ready"); + + let win = window.open("file_sample.html"); + + yield Promise.all([waitForLoad(win), + extension1.awaitFinish("sendmessage_reply"), + extension2.awaitFinish("sendmessage_reply")]); + + win.close(); + + yield extension1.unload(); + yield 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 000000000..e84134eff --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html @@ -0,0 +1,127 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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> +"use strict"; + +add_task(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); + yield extension.startup(); + info("extension loaded"); + + yield extension.awaitMessage("ready"); + + let win = window.open("file_sample.html"); + yield extension.awaitFinish("ping pong done"); + win.close(); + + yield 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 000000000..5764d0a3c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html @@ -0,0 +1,78 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +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(function* test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + let win = window.open("file_sample.html"); + yield Promise.all([waitForLoad(win), extension.awaitMessage("connected")]); + win.close(); + yield extension.awaitMessage("disconnected"); + + info("win.close() succeeded"); + + win = window.open("file_sample.html"); + yield 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"; + yield extension.awaitMessage("disconnected"); + win.close(); + + yield extension.unload(); + info("extension unloaded"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_id.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_id.html new file mode 100644 index 000000000..4cdefda41 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_id.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for browser.runtime.id</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +add_task(function* test_runtime_id() { + function background() { + browser.test.sendMessage("background-id", browser.runtime.id); + } + + function content() { + browser.test.sendMessage("content-id", browser.runtime.id); + } + + let uuidGenerator = SpecialPowers.Cc["@mozilla.org/uuid-generator;1"].getService(SpecialPowers.Ci.nsIUUIDGenerator); + let id = uuidGenerator.generateUUID().number; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + applications: {gecko: {id}}, + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "run_at": "document_start", + "js": ["content_script.js"], + }], + }, + + background, + + files: { + "content_script.js": content, + }, + }); + + yield extension.startup(); + + let backgroundId = yield extension.awaitMessage("background-id"); + is(backgroundId, id, "runtime.id from background script is correct"); + let win = window.open("file_sample.html"); + let contentId = yield extension.awaitMessage("content-id"); + is(contentId, id, "runtime.id from content script is correct"); + + win.close(); + yield extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sandbox_var.html b/toolkit/components/extensions/test/mochitest/test_ext_sandbox_var.html new file mode 100644 index 000000000..426a71ac6 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sandbox_var.html @@ -0,0 +1,60 @@ +<!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/SpawnTask.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"; + +function background() { + browser.runtime.onMessage.addListener(result => { + browser.test.assertEq(result, 12, "x is 12"); + browser.test.notifyPass("background test passed"); + }); +} + +function contentScript() { + window.x = 12; + browser.runtime.onMessage.addListener(function() {}); + browser.runtime.sendMessage(window.x); +} + +let extensionData = { + background, + manifest: { + "content_scripts": [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_idle", + }], + }, + + files: { + "content_script.js": contentScript, + }, +}; + +add_task(function* test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + let win = window.open("file_sample.html"); + + yield Promise.all([waitForLoad(win), extension.awaitFinish()]); + + win.close(); + + yield extension.unload(); + info("extension unloaded"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_schema.html b/toolkit/components/extensions/test/mochitest/test_ext_schema.html new file mode 100644 index 000000000..8a0e11c56 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_schema.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for schema API creation</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="chrome_head.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"; + +add_task(function* testEmptySchema() { + function background() { + browser.test.assertEq(undefined, browser.manifest, "browser.manifest is not defined"); + browser.test.assertTrue("storage" in browser, "browser.storage should be defined"); + browser.test.assertEq(undefined, browser.contextMenus, "browser.contextMenus should not be defined"); + browser.test.notifyPass("schema"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["storage"], + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("schema"); + yield extension.unload(); +}); + +add_task(function* testUnknownProperties() { + function background() { + browser.test.notifyPass("loaded"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["unknownPermission"], + + unknown_property: {}, + }, + + background, + }); + + let messages = [ + {message: /processing permissions\.0: Unknown permission "unknownPermission"/}, + {message: /processing unknown_property: An unexpected property was found in the WebExtension manifest/}, + ]; + + let waitForConsole = new Promise(resolve => { + SimpleTest.monitorConsole(resolve, messages); + }); + + yield extension.startup(); + + yield extension.awaitFinish("loaded"); + + yield extension.unload(); + + SimpleTest.endMonitorConsole(); + yield waitForConsole; +}); +</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 000000000..a3ef37cad --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +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(function* test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + let win = window.open("file_sample.html"); + + yield Promise.all([waitForLoad(win), extension.awaitFinish("sendmessage_doublereply")]); + + win.close(); + + yield extension.unload(); + info("extension unloaded"); +}); +</script> + +</body> +</html> 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 000000000..96af6558e --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html @@ -0,0 +1,83 @@ +<!DOCTYPE html> +<html> +<head> + <title>WebExtension test</title> + <meta charset="utf-8"> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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> +"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(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); + yield extension.startup(); + + let win = window.open("file_sample.html"); + yield extension.awaitFinish("sendMessage callback was invoked"); + win.close(); + + yield extension.unload(); +}); + +add_task(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); + yield extension.startup(); + + let win = window.open("file_sample.html"); + yield extension.awaitFinish("finished chrome.runtime.sendMessage"); + win.close(); + + yield 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 000000000..a4ac708b2 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +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(function* test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + let win = window.open("file_sample.html"); + + yield Promise.all([waitForLoad(win), extension.awaitFinish("sendmessage_reply")]); + + win.close(); + + yield 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 000000000..1ebc1b40f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html @@ -0,0 +1,93 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +function backgroundScript(token) { + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct"); + + if (msg == "done") { + browser.test.notifyPass("sendmessage_reply"); + return; + } + + let tabId = sender.tab.id; + browser.tabs.sendMessage(tabId, `${token}-tabMessage`); + + browser.test.assertEq(msg, token, "token matches"); + sendReply(`${token}-done`); + }); +} + +function contentScript(token) { + let gotTabMessage = false; + let badTabMessage = false; + browser.runtime.onMessage.addListener((msg, sender, sendReply) => { + if (msg == `${token}-tabMessage`) { + gotTabMessage = true; + } else { + badTabMessage = true; + } + }); + + browser.runtime.sendMessage(token, function(resp) { + if (resp != `${token}-done` || !gotTabMessage || badTabMessage) { + return; // test failed + } + 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_start", + }], + }, + + files: { + "content_script.js": `(${contentScript})(${token})`, + }, + }; + return extensionData; +} + +add_task(function* test_contentscript() { + let extension1 = ExtensionTestUtils.loadExtension(makeExtension()); + let extension2 = ExtensionTestUtils.loadExtension(makeExtension()); + + yield Promise.all([extension1.startup(), extension2.startup()]); + + let win = window.open("file_sample.html"); + + yield Promise.all([waitForLoad(win), + extension1.awaitFinish("sendmessage_reply"), + extension2.awaitFinish("sendmessage_reply")]); + + win.close(); + + yield extension1.unload(); + yield extension2.unload(); + info("extensions unloaded"); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_storage_content.html b/toolkit/components/extensions/test/mochitest/test_ext_storage_content.html new file mode 100644 index 000000000..09a33814a --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_content.html @@ -0,0 +1,330 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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="application/javascript"> +"use strict"; + +// Copied from toolkit/components/extensions/test/xpcshell/test_ext_storage.js. +// The storage API in content scripts should behave identical to the storage API +// in background pages. +const STORAGE_SYNC_PREF = "webextensions.storage.sync.enabled"; +/** + * Utility function to ensure that all supported APIs for getting are + * tested. + * + * @param {string} areaName + * either "local" or "sync" according to what we want to test + * @param {string} prop + * "key" to look up using the storage API + * @param {Object} value + * "value" to compare against + */ +async function checkGetImpl(areaName, prop, value) { + let storage = browser.storage[areaName]; + + let data = await storage.get(null); + browser.test.assertEq(value, data[prop], `null getter worked for ${prop} in ${areaName}`); + + data = await storage.get(prop); + browser.test.assertEq(value, data[prop], `string getter worked for ${prop} in ${areaName}`); + + data = await storage.get([prop]); + browser.test.assertEq(value, data[prop], `array getter worked for ${prop} in ${areaName}`); + + data = await storage.get({[prop]: undefined}); + browser.test.assertEq(value, data[prop], `object getter worked for ${prop} in ${areaName}`); +} + +async function contentScript(checkGet) { + let globalChanges, gResolve; + function clearGlobalChanges() { + globalChanges = new Promise(resolve => { gResolve = resolve; }); + } + clearGlobalChanges(); + let expectedAreaName; + + browser.storage.onChanged.addListener((changes, areaName) => { + browser.test.assertEq(expectedAreaName, areaName, + "Expected area name received by listener"); + gResolve(changes); + }); + + async function checkChanges(areaName, changes, message) { + function checkSub(obj1, obj2) { + for (let prop in obj1) { + browser.test.assertTrue(obj1[prop] !== undefined, + `checkChanges ${areaName} ${prop} is missing (${message})`); + browser.test.assertTrue(obj2[prop] !== undefined, + `checkChanges ${areaName} ${prop} is missing (${message})`); + browser.test.assertEq(obj1[prop].oldValue, obj2[prop].oldValue, + `checkChanges ${areaName} ${prop} old (${message})`); + browser.test.assertEq(obj1[prop].newValue, obj2[prop].newValue, + `checkChanges ${areaName} ${prop} new (${message})`); + } + } + + const recentChanges = await globalChanges; + checkSub(changes, recentChanges); + checkSub(recentChanges, changes); + clearGlobalChanges(); + } + + /* eslint-disable dot-notation */ + async function runTests(areaName) { + expectedAreaName = areaName; + let storage = browser.storage[areaName]; + // Set some data and then test getters. + try { + await storage.set({"test-prop1": "value1", "test-prop2": "value2"}); + await checkChanges(areaName, + {"test-prop1": {newValue: "value1"}, "test-prop2": {newValue: "value2"}}, + "set (a)"); + + await checkGet(areaName, "test-prop1", "value1"); + await checkGet(areaName, "test-prop2", "value2"); + + let data = await storage.get({"test-prop1": undefined, "test-prop2": undefined, "other": "default"}); + browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (a)"); + browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (a)"); + browser.test.assertEq("default", data["other"], "other correct"); + + data = await storage.get(["test-prop1", "test-prop2", "other"]); + browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (b)"); + browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (b)"); + browser.test.assertFalse("other" in data, "other correct"); + + // Remove data in various ways. + await storage.remove("test-prop1"); + await checkChanges(areaName, {"test-prop1": {oldValue: "value1"}}, "remove string"); + + data = await storage.get(["test-prop1", "test-prop2"]); + browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove string)"); + browser.test.assertTrue("test-prop2" in data, "prop2 present (remove string)"); + + await storage.set({"test-prop1": "value1"}); + await checkChanges(areaName, {"test-prop1": {newValue: "value1"}}, "set (c)"); + + data = await storage.get(["test-prop1", "test-prop2"]); + browser.test.assertEq(data["test-prop1"], "value1", "prop1 correct (c)"); + browser.test.assertEq(data["test-prop2"], "value2", "prop2 correct (c)"); + + await storage.remove(["test-prop1", "test-prop2"]); + await checkChanges(areaName, + {"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}}, + "remove array"); + + data = await storage.get(["test-prop1", "test-prop2"]); + browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove array)"); + browser.test.assertFalse("test-prop2" in data, "prop2 absent (remove array)"); + + // test storage.clear + await storage.set({"test-prop1": "value1", "test-prop2": "value2"}); + // Make sure that set() handler happened before we clear the + // promise again. + await globalChanges; + + clearGlobalChanges(); + await storage.clear(); + + await checkChanges(areaName, + {"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}}, + "clear"); + data = await storage.get(["test-prop1", "test-prop2"]); + browser.test.assertFalse("test-prop1" in data, "prop1 absent (clear)"); + browser.test.assertFalse("test-prop2" in data, "prop2 absent (clear)"); + + // Make sure we can store complex JSON data. + // known previous values + await storage.set({"test-prop1": "value1", "test-prop2": "value2"}); + + // Make sure the set() handler landed. + await globalChanges; + + clearGlobalChanges(); + await storage.set({ + "test-prop1": { + str: "hello", + bool: true, + null: null, + undef: undefined, + obj: {}, + arr: [1, 2], + date: new Date(0), + regexp: /regexp/, + func: function func() {}, + window, + }, + }); + + await storage.set({"test-prop2": function func() {}}); + const recentChanges = await globalChanges; + + browser.test.assertEq("value1", recentChanges["test-prop1"].oldValue, "oldValue correct"); + browser.test.assertEq("object", typeof(recentChanges["test-prop1"].newValue), "newValue is obj"); + clearGlobalChanges(); + + data = await storage.get({"test-prop1": undefined, "test-prop2": undefined}); + let obj = data["test-prop1"]; + + browser.test.assertEq("hello", obj.str, "string part correct"); + browser.test.assertEq(true, obj.bool, "bool part correct"); + browser.test.assertEq(null, obj.null, "null part correct"); + browser.test.assertEq(undefined, obj.undef, "undefined part correct"); + browser.test.assertEq(undefined, obj.func, "function part correct"); + browser.test.assertEq(undefined, obj.window, "window part correct"); + browser.test.assertEq("1970-01-01T00:00:00.000Z", obj.date, "date part correct"); + browser.test.assertEq("/regexp/", obj.regexp, "regexp part correct"); + browser.test.assertEq("object", typeof(obj.obj), "object part correct"); + browser.test.assertTrue(Array.isArray(obj.arr), "array part present"); + browser.test.assertEq(1, obj.arr[0], "arr[0] part correct"); + browser.test.assertEq(2, obj.arr[1], "arr[1] part correct"); + browser.test.assertEq(2, obj.arr.length, "arr.length part correct"); + + obj = data["test-prop2"]; + + browser.test.assertEq("[object Object]", {}.toString.call(obj), "function serialized as a plain object"); + browser.test.assertEq(0, Object.keys(obj).length, "function serialized as an empty object"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("storage"); + } + } + + browser.test.onMessage.addListener(msg => { + let promise; + if (msg === "test-local") { + promise = runTests("local"); + } else if (msg === "test-sync") { + promise = runTests("sync"); + } + promise.then(() => browser.test.sendMessage("test-finished")); + }); + + browser.test.sendMessage("ready"); +} + +let extensionData = { + manifest: { + content_scripts: [{ + "matches": ["http://mochi.test/*/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_idle", + }], + + permissions: ["storage"], + }, + + files: { + "content_script.js": `(${contentScript})(${checkGetImpl})`, + }, +}; + +add_task(function* test_contentscript() { + let win = window.open("file_sample.html"); + yield waitForLoad(win); + + yield SpecialPowers.pushPrefEnv({ + set: [[STORAGE_SYNC_PREF, true]], + }); + + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield Promise.all([extension.startup(), extension.awaitMessage("ready")]); + extension.sendMessage("test-local"); + yield extension.awaitMessage("test-finished"); + + extension.sendMessage("test-sync"); + yield extension.awaitMessage("test-finished"); + + yield SpecialPowers.popPrefEnv(); + yield extension.unload(); + + win.close(); +}); + +add_task(function* test_local_cache_invalidation() { + let win = window.open("file_sample.html"); + + function background(checkGet) { + browser.test.onMessage.addListener(async msg => { + if (msg === "set-initial") { + await browser.storage.local.set({"test-prop1": "value1", "test-prop2": "value2"}); + browser.test.sendMessage("set-initial-done"); + } else if (msg === "check") { + await checkGet("local", "test-prop1", "value1"); + await checkGet("local", "test-prop2", "value2"); + browser.test.sendMessage("check-done"); + } + }); + + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["storage"], + }, + background: `(${background})(${checkGetImpl})`, + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + extension.sendMessage("set-initial"); + yield extension.awaitMessage("set-initial-done"); + + SpecialPowers.invalidateExtensionStorageCache(); + + extension.sendMessage("check"); + yield extension.awaitMessage("check-done"); + + yield extension.unload(); + win.close(); +}); + +add_task(function* test_config_flag_needed() { + let win = window.open("file_sample.html"); + yield waitForLoad(win); + + function background() { + let promises = []; + let apiTests = [ + {method: "get", args: ["foo"]}, + {method: "set", args: [{foo: "bar"}]}, + {method: "remove", args: ["foo"]}, + {method: "clear", args: []}, + ]; + apiTests.forEach(testDef => { + promises.push(browser.test.assertRejects( + browser.storage.sync[testDef.method](...testDef.args), + "Please set webextensions.storage.sync.enabled to true in about:config", + `storage.sync.${testDef.method} is behind a flag`)); + }); + + Promise.all(promises).then(() => browser.test.notifyPass("flag needed")); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["storage"], + }, + background: `(${background})(${checkGetImpl})`, + }); + + yield extension.startup(); + yield extension.awaitFinish("flag needed"); + yield extension.unload(); + win.close(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_storage_tab.html b/toolkit/components/extensions/test/mochitest/test_ext_storage_tab.html new file mode 100644 index 000000000..32d8e6af0 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_tab.html @@ -0,0 +1,118 @@ +<!DOCTYPE html> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +add_task(function* test_multiple_pages() { + async function background() { + let tabReady = new Promise(resolve => { + browser.runtime.onMessage.addListener(function listener(msg) { + browser.test.log("onMessage " + msg); + if (msg == "tab-ready") { + browser.runtime.onMessage.removeListener(listener); + resolve(); + } + }); + }); + + let tabId; + let tabRemoved = new Promise(resolve => { + browser.tabs.onRemoved.addListener(function listener(removedId) { + if (removedId == tabId) { + browser.tabs.onRemoved.removeListener(listener); + + // Delay long enough to be sure the inner window has been nuked. + setTimeout(resolve, 0); + } + }); + }); + + try { + let storage = browser.storage.local; + + browser.test.log("create"); + let tab = await browser.tabs.create({url: "tab.html"}); + tabId = tab.id; + + await tabReady; + + let result = await storage.get("key"); + browser.test.assertEq(undefined, result.key, "Key should be undefined"); + + await browser.runtime.sendMessage("tab-set-key"); + + result = await storage.get("key"); + browser.test.assertEq(JSON.stringify({foo: {bar: "baz"}}), + JSON.stringify(result.key), + "Key should be set to the value from the tab"); + + browser.test.log("Remove tab"); + + await Promise.all([ + browser.tabs.remove(tabId), + tabRemoved, + ]); + + result = await storage.get("key"); + browser.test.assertEq(JSON.stringify({foo: {bar: "baz"}}), + JSON.stringify(result.key), + "Key should still be set to the value from the tab"); + + browser.test.notifyPass("storage-multiple"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("storage-multiple"); + } + } + + function tab() { + browser.test.log("tab"); + browser.runtime.onMessage.addListener(msg => { + if (msg == "tab-set-key") { + return browser.storage.local.set({key: {foo: {bar: "baz"}}}); + } + }); + + browser.runtime.sendMessage("tab-ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + + files: { + "tab.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <script src="tab.js"><\/script> + </head> + </html>`, + + "tab.js": tab, + }, + + manifest: { + permissions: ["storage"], + }, + }); + + yield extension.startup(); + + yield extension.awaitFinish("storage-multiple"); + yield 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 000000000..1f3a9a3c9 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html @@ -0,0 +1,202 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +add_task(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); + + yield extension.startup(); + + yield extension.awaitFinish("webext-tab-subframe-privileges"); + yield extension.unload(); +}); + +add_task(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); + + yield extension.startup(); + + yield extension.awaitFinish("webext-background-subframe-privileges"); + yield extension.unload(); +}); + +add_task(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); + + yield extension.startup(); + + let win = window.open("http://example.com"); + + yield extension.awaitFinish("webext-contentscript-subframe-privileges"); + + win.close(); + + yield extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tab_teardown.html b/toolkit/components/extensions/test/mochitest/test_ext_tab_teardown.html new file mode 100644 index 000000000..dc351e48a --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tab_teardown.html @@ -0,0 +1,150 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for extension tab teardown</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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> +"use strict"; + +// Test for tabs opened using tabs.create and window.open +function* runTabReloadAndCloseTest(extension) { + let chromeScript = SpecialPowers.loadChromeScript( + SimpleTest.getTestFileURL("file_teardown_test.js")); + yield chromeScript.promiseOneMessage("chromescript-startup"); + function* getContextEvents() { + chromeScript.sendAsyncMessage("get-context-events"); + let contextEvents = yield chromeScript.promiseOneMessage("context-events"); + dump(JSON.stringify(contextEvents)); + return contextEvents.filter(event => event.extensionId == extension.id); + } + + extension.sendMessage("open extension page"); + let extensionPageUrl = yield extension.awaitMessage("extension page loaded"); + + let contextEvents = yield* getContextEvents(); + is(contextEvents.length, 1, "ExtensionContext change for opening a tab"); + is(contextEvents[0].eventType, "load", "create ExtensionContext for tab"); + is(contextEvents[0].url, extensionPageUrl, + "ExtensionContext URL after tab creation should be tab URL"); + + extension.sendMessage("reload extension page"); + let extensionPageUrl2 = yield extension.awaitMessage("extension page loaded"); + + is(extensionPageUrl, extensionPageUrl2, + "The tab's URL is expected to not change after a page reload"); + + contextEvents = yield* getContextEvents(); + is(contextEvents.length, 2, "ExtensionContext change after tab reload"); + is(contextEvents[0].eventType, "unload", "unload old ExtensionContext"); + is(contextEvents[0].url, extensionPageUrl, + "ExtensionContext URL before reload should be tab URL"); + is(contextEvents[1].eventType, "load", "create new ExtensionContext for tab"); + is(contextEvents[1].url, extensionPageUrl2, + "ExtensionContext URL after reload should be tab URL"); + + extension.sendMessage("close extension page"); + yield extension.awaitMessage("closed extension page"); + + contextEvents = yield* getContextEvents(); + is(contextEvents.length, 1, "ExtensionContext after closing tab"); + is(contextEvents[0].eventType, "unload", "unload tab's ExtensionContext"); + is(contextEvents[0].url, extensionPageUrl2, + "ExtensionContext URL at closing tab should be tab URL"); + + chromeScript.sendAsyncMessage("cleanup"); + chromeScript.destroy(); + yield extension.unload(); +} + +add_task(function* test_extension_page_tabs_create_reload_and_close() { + function background() { + let tabId; + browser.test.onMessage.addListener(msg => { + if (msg === "open extension page") { + chrome.tabs.create({url: "page.html"}, tab => { + tabId = tab.id; + }); + } else if (msg === "reload extension page") { + chrome.tabs.reload(tabId); + } else if (msg === "close extension page") { + chrome.tabs.remove(tabId, () => { + browser.test.sendMessage("closed extension page"); + }); + } + }); + } + + function pageScript() { + browser.test.sendMessage("extension page loaded", document.URL); + } + + let extensionData = { + background, + files: { + "page.html": `<!DOCTYPE html><meta charset="utf-8"><script src="page.js"><\/script>`, + "page.js": pageScript, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + yield* runTabReloadAndCloseTest(extension); +}); + +add_task(function* test_extension_page_window_open_reload_and_close() { + // This tests whether a context that is opened via window.open is properly + // disposed when the tab closes. + // The background page cannot use window.open (bugzil.la/1282021), so we open + // another extension page that manages the window.open-tab for testing. + function background() { + chrome.tabs.create({url: "window.open.html"}); + } + + function windowOpenScript() { + let win; + browser.test.onMessage.addListener(msg => { + if (msg === "open extension page") { + win = window.open("page.html"); + } else if (msg === "reload extension page") { + win.location.reload(); + } else if (msg === "close extension page") { + browser.tabs.onRemoved.addListener(function listener() { + browser.tabs.onRemoved.removeListener(listener); + browser.test.sendMessage("closed extension page"); + }); + win.close(); + } + }); + browser.test.sendMessage("setup-intermediate-tab"); + } + + function pageScript() { + browser.test.sendMessage("extension page loaded", document.URL); + } + + let extensionData = { + background, + files: { + "page.html": `<!DOCTYPE html><meta charset="utf-8"><script src="page.js"><\/script>`, + "page.js": pageScript, + "window.open.html": `<!DOCTYPE html><meta charset="utf-8"><script src="window.open.js"><\/script>`, + "window.open.js": windowOpenScript, + }, + }; + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + yield extension.awaitMessage("setup-intermediate-tab"); + yield* runTabReloadAndCloseTest(extension); +}); +</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 000000000..fef31e0e2 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_test.html @@ -0,0 +1,191 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Testing test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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.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-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(function* test_test_in_background() { + let extensionData = { + background: `(${testScript})()`, + }; + + let extension = loadExtensionAndInterceptTest(extensionData); + yield extension.startup(); + let results = yield extension.awaitResults(); + verifyTestResults(results, "background page", "moz-extension:"); + yield extension.unload(); +}); + +add_task(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); + yield extension.startup(); + let win = window.open("file_sample.html"); + let results = yield extension.awaitResults(); + win.close(); + verifyTestResults(results, "content script", "http:"); + yield extension.unload(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_unload_frame.html b/toolkit/components/extensions/test/mochitest/test_ext_unload_frame.html new file mode 100644 index 000000000..5572de281 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_unload_frame.html @@ -0,0 +1,170 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtensions test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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> +"use strict"; + +/* globals delayedNotifyPass */ // Available in the background page of the test extensions. + +// Background and content script for testSendMessage_* +function sendMessage_background() { + browser.runtime.onMessage.addListener((msg, sender, sendResponse) => { + browser.test.assertEq("from frame", msg, "Expected message from frame"); + sendResponse("msg from back"); // Should not throw or anything like that. + delayedNotifyPass("Received sendMessage from closing frame"); + }); +} +function sendMessage_contentScript(testType) { + browser.runtime.sendMessage("from frame", reply => { + // The frame has been removed, so we should not get this callback! + browser.test.fail(`Unexpected reply: ${reply}`); + }); + if (testType == "frame") { + frameElement.remove(); + } else { + window.close(); + } +} + +// Background and content script for testConnect_* +function connect_background() { + browser.runtime.onConnect.addListener(port => { + browser.test.assertEq("port from frame", port.name); + + let disconnected = false; + let hasMessage = false; + port.onDisconnect.addListener(() => { + browser.test.assertFalse(disconnected, "onDisconnect should fire once"); + disconnected = true; + browser.test.assertTrue(hasMessage, "Expected onMessage before onDisconnect"); + browser.test.assertEq(null, port.error, "The port is implicitly closed without errors when the other context unloads"); + delayedNotifyPass("Received onDisconnect from closing frame"); + }); + port.onMessage.addListener(msg => { + browser.test.assertFalse(hasMessage, "onMessage should fire once"); + hasMessage = true; + browser.test.assertFalse(disconnected, "Should get message before disconnect"); + browser.test.assertEq("from frame", msg, "Expected message from frame"); + }); + + port.postMessage("reply to closing frame"); + }); +} +function connect_contentScript(testType) { + let isUnloading = false; + addEventListener("pagehide", () => { isUnloading = true; }, {once: true}); + + let port = browser.runtime.connect({name: "port from frame"}); + port.onMessage.addListener(msg => { + // The background page sends a reply as soon as we call runtime.connect(). + // It is possible that the reply reaches this frame before the + // window.close() request has been processed. + if (!isUnloading) { + browser.test.log(`Ignorting unexpected reply ("${msg}") because the page is not being unloaded.`); + return; + } + + // The frame has been removed, so we should not get a reply. + browser.test.fail(`Unexpected reply: ${msg}`); + }); + port.postMessage("from frame"); + + // Removing the frame or window should disconnect the port. + if (testType == "frame") { + frameElement.remove(); + } else { + window.close(); + } +} + +// `testType` is "window" or "frame". +function createTestExtension(testType, backgroundScript, contentScript) { + // Make a roundtrip between the background page and the test runner (which is + // in the same process as the content script) to make sure that we record a + // failure in case the content script's sendMessage or onMessage handlers are + // called even after the frame or window was removed. + function delayedNotifyPass(msg) { + browser.test.onMessage.addListener((type, echoMsg) => { + if (type == "pong") { + browser.test.assertEq(msg, echoMsg, "Echoed reply should be the same"); + browser.test.notifyPass(msg); + } + }); + browser.test.log("Starting ping-pong to flush messages..."); + browser.test.sendMessage("ping", msg); + } + let extension = ExtensionTestUtils.loadExtension({ + background: `${delayedNotifyPass};(${backgroundScript})();`, + manifest: { + content_scripts: [{ + js: ["contentscript.js"], + all_frames: testType == "frame", + matches: ["http://mochi.test/*/file_sample.html"], + }], + }, + files: { + "contentscript.js": `(${contentScript})("${testType}");`, + }, + }); + extension.awaitMessage("ping").then(msg => { + extension.sendMessage("pong", msg); + }); + return extension; +} + +add_task(function* testSendMessage_and_remove_frame() { + let extension = createTestExtension("frame", sendMessage_background, sendMessage_contentScript); + yield extension.startup(); + + let frame = document.createElement("iframe"); + frame.src = "file_sample.html"; + document.body.appendChild(frame); + + yield extension.awaitFinish("Received sendMessage from closing frame"); + yield extension.unload(); +}); + +add_task(function* testConnect_and_remove_frame() { + let extension = createTestExtension("frame", connect_background, connect_contentScript); + yield extension.startup(); + + let frame = document.createElement("iframe"); + frame.src = "file_sample.html"; + document.body.appendChild(frame); + + yield extension.awaitFinish("Received onDisconnect from closing frame"); + yield extension.unload(); +}); + +add_task(function* testSendMessage_and_remove_window() { + let extension = createTestExtension("window", sendMessage_background, sendMessage_contentScript); + yield extension.startup(); + + window.open("file_sample.html"); + + yield extension.awaitFinish("Received sendMessage from closing frame"); + yield extension.unload(); +}); + +add_task(function* testConnect_and_remove_window() { + let extension = createTestExtension("window", connect_background, connect_contentScript); + yield extension.startup(); + + window.open("file_sample.html"); + + yield extension.awaitFinish("Received onDisconnect from closing frame"); + yield extension.unload(); +}); + +</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 000000000..fa3228739 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html @@ -0,0 +1,353 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the web_accessible_resources manifest directive</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +/* 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"); + testImage.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(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"); + iframe.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]);', + }, + }); + + yield extension.startup(); + + yield extension.awaitMessage("ready"); + + let win = window.open("http://example.com/"); + + yield extension.awaitFinish("web-accessible-resources"); + + win.close(); + + yield extension.unload(); +}); + +add_task(function* test_web_accessible_resources_csp() { + function background() { + browser.runtime.onMessage.addListener((msg, sender) => { + 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); + } + }); + + browser.test.sendMessage("background-ready"); + } + + function content() { + window.addEventListener("message", function rcv(event) { + browser.runtime.sendMessage("script-ran"); + window.removeEventListener("message", rcv, false); + }, false); + + testImageLoading(browser.extension.getURL("image.png"), "loaded"); + + let testScriptElement = document.createElement("script"); + testScriptElement.setAttribute("src", browser.extension.getURL("test_script.js")); + document.head.appendChild(testScriptElement); + browser.runtime.sendMessage("script-loaded"); + } + + function testScript() { + window.postMessage("test-script-loaded", "*"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "content_scripts": [{ + "matches": ["http://example.com/*/file_csp.html"], + "run_at": "document_start", + "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, + }, + }); + + // This is used to watch the blocked data bounce off CSP. + function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + } + + let cspEventCount = 0; + + examiner.prototype = { + observe: function(subject, topic, data) { + cspEventCount++; + let spec = SpecialPowers.wrap(subject).QueryInterface(SpecialPowers.Ci.nsIURI).spec; + ok(spec.includes("file_image_bad.png") || spec.includes("file_script_bad.js"), + `Expected file: ${spec} rejected by CSP`); + }, + + // We must eventually call this to remove the listener, + // or mochitests might get borked. + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + }, + }; + + let observer = new examiner(); + + yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]); + + let win = window.open("http://example.com/tests/toolkit/components/extensions/test/mochitest/file_csp.html"); + + yield Promise.all([ + extension.awaitMessage("image-loaded"), + extension.awaitMessage("script-loaded"), + extension.awaitMessage("script-ran"), + ]); + is(cspEventCount, 2, "Two items were rejected by CSP"); + win.close(); + + observer.remove(); + yield extension.unload(); +}); + +add_task(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"); + testScriptElement.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_start", + "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); + + yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]); + + let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_mixed.html"); + + yield Promise.all([ + extension.awaitMessage("image-blocked"), + extension.awaitMessage("image-loaded"), + extension.awaitMessage("accessible-script-loaded"), + ]); + yield extension.awaitFinish("mixed-test"); + win.close(); + + yield 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 000000000..2287fd9b1 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html @@ -0,0 +1,559 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" 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"; + +/* 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(details.frameId, 0, "root frame ID correct"); + browser.test.assertEq(details.parentFrameId, -1, "root parent frame ID correct"); + } else { + browser.test.assertEq(details.parentFrameId, 0, "parent frame ID correct"); + browser.test.assertTrue(details.frameId != 0, "frame ID probably okay"); + } + + browser.test.assertTrue(details.frameId !== undefined); + browser.test.assertTrue(details.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 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(function* webnav_transitions_props() { + function backgroundScriptTransitions() { + const EVENTS = [ + "onCommitted", + "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(); + } + }); + + yield Promise.all([extension.startup(), extension.awaitMessage("ready")]); + info("webnavigation extension loaded"); + + let win = window.open(); + + yield loadAndWait(win, "onCompleted", URL, () => { win.location = URL; }); + + // transitionType: reload + received = []; + yield 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 = []; + yield loadAndWait(win, "onCompleted", URL, () => { + win.document.querySelector("form").submit(); + }); + + found = received.find((data) => (data.event == "onCommitted" && data.url == 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 = []; + yield 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 = []; + yield loadAndWait(win, "onCompleted", URL, () => { win.history.back(); }); + + found = received.find((data) => (data.event == "onCommitted" && data.url == 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 = []; + yield 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 = []; + yield 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 = []; + yield 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 = []; + yield 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"); + // 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 = []; + yield 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 = []; + yield 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) { + is(found.details.transitionType, "manual_subframe", + "Got the expected 'manual_subframe' transitionType in the OnCommitted event"); + } + + // cleanup phase + win.close(); + + yield extension.unload(); + info("webnavigation extension unloaded"); +}); + +add_task(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(); + } + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + info("webnavigation extension loaded"); + + let win = window.open(); + + yield 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}); + } + + yield 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}`); + yield 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}`); + yield 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}`); + yield loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.forward(); }); + info(`Received ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`); + } + + win.close(); + + yield extension.unload(); + info("webnavigation extension unloaded"); +}); + +add_task(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(); + } + }); + + yield Promise.all([extension.startup(), extension.awaitMessage("ready")]); + info("webnavigation extension loaded"); + + let win = window.open(); + + received = []; + yield 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(); + + yield 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 000000000..a0de5e9e5 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html @@ -0,0 +1,308 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +add_task(function* test_webnav_unresolved_uri_on_expected_URI_scheme() { + function background() { + let lastTest; + + function cleanupTestListeners() { + if (lastTest) { + let {event, okListener, failListener} = lastTest; + lastTest = null; + browser.test.log(`Cleanup previous test event listeners`); + browser.webNavigation[event].removeListener(okListener); + browser.webNavigation[event].removeListener(failListener); + } + } + + function createTestListener(event, fail, urlFilter) { + 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}`); + } + + cleanupTestListeners(); + browser.test.sendMessage("test-filter-next"); + } + + browser.webNavigation[event].addListener(listener, urlFilter); + + return listener; + } + + browser.test.onMessage.addListener((msg, event, okFilter, failFilter) => { + if (msg !== "test-filter") { + return; + } + + lastTest = { + event, + // Register the failListener first, which should not be called + // and if it is called the test scenario is marked as a failure. + failListener: createTestListener(event, true, failFilter), + okListener: createTestListener(event, false, okFilter), + }; + + browser.test.sendMessage("test-filter-ready"); + }); + + browser.test.sendMessage("ready"); + } + + let extensionData = { + manifest: { + permissions: [ + "webNavigation", + ], + }, + background, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + yield extension.startup(); + + yield extension.awaitMessage("ready"); + + 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]]}], + }, + ], + }, + { + 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"}], + }, + ], + }, + { + url: "http://example.net/browser", + filters: [ + // multiple criteria in a single filter: + // if one of the critera 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 critera is verified, the event should be received. + { + okFilter: [{schemes: ["https"]}, {ports: [80, 22, 443]}], + failFilter: [{schemes: ["https"]}, {ports: [81, 82, 83]}], + }, + ], + }, + ]; + + function* runTestScenario(event, {url, filters}) { + for (let testFilters of filters) { + let {okFilter, failFilter} = testFilters; + + info(`Prepare the new test scenario: ${event} ${url} ${JSON.stringify(testFilters)}`); + win.location = "about:blank"; + + extension.sendMessage("test-filter", event, {url: okFilter}, {url: failFilter}); + yield extension.awaitMessage("test-filter-ready"); + + info(`Loading the test url: ${url}`); + win.location = url; + + yield extension.awaitMessage("test-filter-next"); + + info("Test scenario completed. Moving to the next test scenario."); + } + } + + const BASE_WEBNAV_EVENTS = [ + "onBeforeNavigate", + "onCommitted", + "onDOMContentLoaded", + "onCompleted", + ]; + + info("WebNavigation event filters test scenarios starting..."); + + for (let filterScenario of testFilterScenarios) { + for (let event of BASE_WEBNAV_EVENTS) { + yield runTestScenario(event, filterScenario); + } + } + + 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 event = "onCompleted"; + + info(`Loading the initial test url: ${url}`); + extension.sendMessage("test-filter", event, {url: okFilter}, {url: failFilter}); + + yield extension.awaitMessage("test-filter-ready"); + win.location = url; + yield extension.awaitMessage("test-filter-next"); + + event = "onReferenceFragmentUpdated"; + extension.sendMessage("test-filter", event, {url: okFilter}, {url: failFilter}); + + yield extension.awaitMessage("test-filter-ready"); + win.location = url + "#ref1"; + yield extension.awaitMessage("test-filter-next"); + + info("WebNavigation event filters test onHistoryStateUpdated scenario starting..."); + + event = "onHistoryStateUpdated"; + extension.sendMessage("test-filter", event, {url: okFilter}, {url: failFilter}); + yield extension.awaitMessage("test-filter-ready"); + + win.history.pushState({}, "", BASE + "/pushState_page3.html"); + yield 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."); + + yield extension.unload(); + + win.close(); +}); + +add_task(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, + }); + + yield extension.startup(); + + yield extension.awaitFinish("webNav.emptyFilterValidationError"); + + yield extension.unload(); +}); + +</script> + +</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 000000000..78efeab35 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html @@ -0,0 +1,116 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +add_task(function* test_webRequest_serviceworker_events() { + yield SpecialPowers.pushPrefEnv({ + set: [["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.openWindow.enabled", true], + ], + }); + + 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), `recieved ${name}`); + eventNames.delete(name); + 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/*"]} + ); + } + }, + }); + + yield extension.startup(); + let registration = yield navigator.serviceWorker.register("webrequest_worker.js", {scope: "."}); + yield extension.awaitMessage("done"); + yield registration.unregister(); + yield extension.unload(); +}); + +add_task(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), `recieved ${name}`); + eventNames.delete(name); + + if (eventNames.size === 0) { + browser.test.assertEq(0, eventNames.size, "messages recieved"); + 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.pass("Fetch succeeded."); + }, () => { + browser.test.fail("fetch recieved"); + browser.test.sendMessage("done"); + }); + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("done"); + yield 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 000000000..ef77fee3b --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html @@ -0,0 +1,327 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" 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"; + +let extension; +add_task(function* setup() { + // SelfSupport has a tendency to fire when running this test alone, without + // a good way to turn it off we just set the url to "" + yield SpecialPowers.pushPrefEnv({ + set: [["browser.selfsupport.url", ""]], + }); + + // 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); + + extension = makeExtension(); + yield 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 + +add_task(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}); + yield extension.awaitMessage("continue"); + addStylesheet("file_style_bad.css"); + yield extension.awaitMessage("cancelled"); + // we redirect to style_good which completes the test + addStylesheet("file_style_redirect.css"); + yield extension.awaitMessage("done"); + + let style = window.getComputedStyle(document.getElementById("test"), null); + is(style.getPropertyValue("color"), "rgb(255, 0, 0)", "Good CSS loaded"); +}); + +add_task(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}); + yield extension.awaitMessage("continue"); + addImage("file_image_bad.png"); + yield extension.awaitMessage("cancelled"); + // we redirect to image_good which completes the test + addImage("file_image_redirect.png"); + yield extension.awaitMessage("done"); +}); + +add_task(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}); + yield extension.awaitMessage("continue"); + addScript("file_script_bad.js"); + yield extension.awaitMessage("cancelled"); + // we redirect to script_good which completes the test + addScript("file_script_redirect.js"); + yield extension.awaitMessage("done"); + + is(window.success, 1, "Good script ran"); + is(window.failure, undefined, "Failure script didn't run"); +}); + +add_task(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}); + yield extension.awaitMessage("continue"); + addScript("file_script_xhr.js"); + yield extension.awaitMessage("done"); +}); + +add_task(function* test_webRequest_nonexistent() { + let expect = { + "nonexistent_script_url.js": { + status: 404, + type: "script", + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + yield extension.awaitMessage("continue"); + addScript("nonexistent_script_url.js"); + yield extension.awaitMessage("done"); +}); + +add_task(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}); + yield extension.awaitMessage("continue"); + addImage("file_image_good.png"); + addScript("file_script_good.js"); + addStylesheet("file_style_good.css"); + addScript("nonexistent_script_url.js"); + yield extension.awaitMessage("done"); + + is(window.success, 2, "Good script ran"); + is(window.failure, undefined, "Failure script didn't run"); +}); + +add_task(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}); + yield extension.awaitMessage("continue"); + addScript("file_script_nonexistent.js"); + yield extension.awaitMessage("done"); +}); + +add_task(function* test_webRequest_tabId() { + let expect = { + "file_WebRequest_page3.html": { + type: "main_frame", + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + yield extension.awaitMessage("continue"); + let a = addLink("file_WebRequest_page3.html?trigger=a"); + a.click(); + yield extension.awaitMessage("done"); +}); + +add_task(function* test_webRequest_tabId_browser() { + async function background(url) { + 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 tabExt = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "tabs", + ], + }, + background: `(${background})('${SimpleTest.getTestFileURL("file_sample.html")}?nocache=${Math.random()}')`, + }); + + let expect = { + "file_sample.html": { + type: "main_frame", + }, + }; + // expecting origin == undefined + extension.sendMessage("set-expected", {expect}); + yield extension.awaitMessage("continue"); + + // open a tab from a system principal + yield tabExt.startup(); + + yield extension.awaitMessage("done"); + tabExt.sendMessage("done"); + yield tabExt.awaitMessage("done"); + yield tabExt.unload(); +}); + +add_task(function* test_webRequest_frames() { + let expect = { + "text/plain,webRequestTest": { + type: "sub_frame", + events: ["onBeforeRequest", "onCompleted"], + }, + "text/plain,webRequestTest_bad": { + type: "sub_frame", + events: ["onBeforeRequest", "onCompleted"], + cancel: "onBeforeRequest", + }, + "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"], + }, + }; + extension.sendMessage("set-expected", {expect, origin: location.href}); + yield extension.awaitMessage("continue"); + addFrame("data:text/plain,webRequestTest"); + addFrame("data:text/plain,webRequestTest_bad"); + yield extension.awaitMessage("cancelled"); + addFrame("redirection.sjs"); + addFrame("https://invalid.localhost/badrobot"); + yield extension.awaitMessage("done"); +}); + +add_task(function* teardown() { + yield extension.unload(); +}); +</script> +</head> +<body> +<div id="test">Sample text</div> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_suspend.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_suspend.html new file mode 100644 index 000000000..c8423ec7c --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_suspend.html @@ -0,0 +1,216 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for simple WebExtension</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +add_task(function* test_suspend() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + ], + }, + + background() { + browser.webRequest.onBeforeSendHeaders.addListener( + details => { + // Make sure that returning undefined or a promise that resolves to + // undefined does not break later handlers. + }, + {urls: ["<all_urls>"]}, + ["blocking", "requestHeaders"]); + + browser.webRequest.onBeforeSendHeaders.addListener( + details => { + return Promise.resolve(); + }, + {urls: ["<all_urls>"]}, + ["blocking", "requestHeaders"]); + + browser.webRequest.onBeforeSendHeaders.addListener( + details => { + let requestHeaders = details.requestHeaders.concat({name: "Foo", value: "Bar"}); + + return new Promise(resolve => { + setTimeout(resolve, 500); + }).then(() => { + return {requestHeaders}; + }); + }, + {urls: ["<all_urls>"]}, + ["blocking", "requestHeaders"]); + }, + }); + + yield extension.startup(); + + let result = yield fetch(SimpleTest.getTestFileURL("return_headers.sjs")); + + let headers = JSON.parse(yield result.text()); + + is(headers.foo, "Bar", "Request header was correctly set on suspended request"); + + yield extension.unload(); +}); + + +// Test that requests that were canceled while suspended for a blocking +// listener are correctly resumed. +add_task(function* test_error_resume() { + let chromeScript = SpecialPowers.loadChromeScript(() => { + const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + Cu.import("resource://gre/modules/Services.jsm"); + + let observer = channel => { + if (channel instanceof Ci.nsIHttpChannel && channel.URI.spec === "http://example.com/") { + Services.obs.removeObserver(observer, "http-on-modify-request"); + + // Wait until the next tick to make sure this runs after WebRequest observers. + Promise.resolve().then(() => { + channel.cancel(Cr.NS_BINDING_ABORTED); + }); + } + }; + + Services.obs.addObserver(observer, "http-on-modify-request", false); + }); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + ], + }, + + background() { + browser.webRequest.onBeforeSendHeaders.addListener( + details => { + browser.test.log(`onBeforeSendHeaders({url: ${details.url}})`); + + if (details.url === "http://example.com/") { + browser.test.sendMessage("got-before-send-headers"); + } + }, + {urls: ["<all_urls>"]}, + ["blocking"]); + + browser.webRequest.onErrorOccurred.addListener( + details => { + browser.test.log(`onErrorOccurred({url: ${details.url}})`); + + if (details.url === "http://example.com/") { + browser.test.sendMessage("got-error-occurred"); + } + }, + {urls: ["<all_urls>"]}); + }, + }); + + yield extension.startup(); + + try { + yield fetch("http://example.com/"); + ok(false, "Fetch should have failed."); + } catch (e) { + ok(true, "Got expected error."); + } + + yield extension.awaitMessage("got-before-send-headers"); + yield extension.awaitMessage("got-error-occurred"); + + yield extension.unload(); + chromeScript.destroy(); +}); + + +// Test that response header modifications take effect before onStartRequest fires. +add_task(function* test_set_responseHeaders() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + "http://example.com/", + ], + }, + + background() { + browser.webRequest.onHeadersReceived.addListener( + details => { + browser.test.log(`onHeadersReceived({url: ${details.url}})`); + + details.responseHeaders.push({name: "foo", value: "bar"}); + + return {responseHeaders: details.responseHeaders}; + }, + {urls: ["http://example.com/?modify_headers"]}, + ["blocking", "responseHeaders"]); + }, + }); + + yield extension.startup(); + + yield new Promise(resolve => setTimeout(resolve, 0)); + + let chromeScript = SpecialPowers.loadChromeScript(() => { + const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + + Cu.import("resource://gre/modules/NetUtil.jsm"); + Cu.import("resource://gre/modules/Services.jsm"); + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + + let ssm = Services.scriptSecurityManager; + + let channel = NetUtil.newChannel({ + uri: "http://example.com/?modify_headers", + loadingPrincipal: ssm.createCodebasePrincipalFromOrigin("http://example.com"), + contentPolicyType: Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + }); + + channel.asyncOpen2({ + QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener]), + + onStartRequest(request, context) { + request.QueryInterface(Ci.nsIHttpChannel); + + try { + sendAsyncMessage("response-header-foo", request.getResponseHeader("foo")); + } catch (e) { + sendAsyncMessage("response-header-foo", null); + } + request.cancel(Cr.NS_BINDING_ABORTED); + }, + + onStopRequest() { + }, + + onDataAvailable() { + throw new Components.Exception("", Cr.NS_ERROR_FAILURE); + }, + }); + }); + + let headerValue = yield chromeScript.promiseOneMessage("response-header-foo"); + is(headerValue, "bar", "Expected Foo header value"); + + yield extension.unload(); + chromeScript.destroy(); +}); + + +</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 000000000..998ab9800 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html @@ -0,0 +1,199 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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> + +<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(function* test_setup() { + files = yield 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>"]}; + + let requestBodySupported = true; + + function onUpload(details) { + let url = new URL(details.url); + let upload = url.searchParams.get("upload"); + if (!upload || !requestBodySupported) { + 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}`); + + browser.test.sendMessage("done"); + }, + FILTERS); + + let onBeforeRequest = details => { + browser.test.log(`${name} ${details.requestId} ${details.url}`); + + onUpload(details); + }; + + try { + browser.webRequest.onBeforeRequest.addListener( + onBeforeRequest, FILTERS, ["requestBody"]); + } 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; + } + + browser.webRequest.onBeforeRequest.addListener( + onBeforeRequest, FILTERS); + } +} + +add_task(function* test_xhr_forms() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + "<all_urls>", + ], + }, + background, + }); + + yield extension.startup(); + + 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(); + yield extension.awaitMessage("done"); + + 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 extension.awaitMessage("done"); + }; + + formData.append(blob.name, blob.content, blob.fileName); + formData.append("formDataField", "some value"); + updateActionURL(); + yield post(formData); + + action.searchParams.set("upload", JSON.stringify([{file: "<file>"}])); + yield post(testFile); + + action.searchParams.set("upload", `${blob.content.size} bytes`); + yield post(blob.content); + + let byteLength = 16; + action.searchParams.set("upload", `${byteLength} bytes`); + yield post(new ArrayBuffer(byteLength)); + } + + yield 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 000000000..7d49d55ba --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html @@ -0,0 +1,105 @@ +<!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/SpawnTask.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"; + +/* eslint-disable mozilla/balanced-listeners */ + +add_task(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); + yield extension.startup(); + + let iframeURL = yield extension.awaitMessage("iframe-url"); + let testURL = SimpleTest.getTestFileURL("file_sample.html"); + + for (let url of [iframeURL, testURL]) { + info(`Testing URL ${url}`); + + let iframe = yield createIframe(url); + + iframe.contentWindow.postMessage( + "ping", url); + + let pong = yield awaitMessage(); + is(pong.pong, url, "Got expected pong"); + + iframe.remove(); + } + + yield extension.unload(); +}); +</script> + +</body> +</html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_xhr_capabilities.html b/toolkit/components/extensions/test/mochitest/test_ext_xhr_capabilities.html new file mode 100644 index 000000000..1afdadb9f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_xhr_capabilities.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test XHR capabilities</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"; + +add_task(function* test_xhr_capabilities() { + function backgroundScript() { + let xhr = new XMLHttpRequest(); + xhr.open("GET", browser.extension.getURL("bad.xml")); + + browser.test.sendMessage("result", + {name: "Background script XHRs should not be privileged", + result: xhr.channel === undefined}); + + xhr.onload = () => { + browser.test.sendMessage("result", + {name: "Background script XHRs should not yield <parsererrors>", + result: xhr.responseXML === null}); + }; + xhr.send(); + } + + function contentScript() { + let xhr = new XMLHttpRequest(); + xhr.open("GET", browser.extension.getURL("bad.xml")); + + browser.test.sendMessage("result", + {name: "Content script XHRs should not be privileged", + result: xhr.channel === undefined}); + + xhr.onload = () => { + browser.test.sendMessage("result", + {name: "Content script XHRs should not yield <parsererrors>", + result: xhr.responseXML === null}); + }; + xhr.send(); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + background: { + "scripts": ["background.js"], + }, + content_scripts: [{ + "matches": ["http://example.com/"], + "js": ["content_script.js"], + }], + web_accessible_resources: [ + "bad.xml", + ], + }, + + files: { + "bad.xml": "<xml", + "background.js": `(${backgroundScript})()`, + "content_script.js": `(${contentScript})()`, + }, + }); + + yield extension.startup(); + + let win = window.open("http://example.com/"); + + // We expect four test results from the content/background scripts. + for (let i = 0; i < 4; ++i) { + let result = yield extension.awaitMessage("result"); + ok(result.result, result.name); + } + + win.close(); + yield extension.unload(); +}); +</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 000000000..ccfb2ac1f --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js @@ -0,0 +1,8 @@ +"use strict"; + +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 000000000..bfb148301 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/webrequest_test.jsm @@ -0,0 +1,22 @@ +"use strict"; + +this.EXPORTED_SYMBOLS = ["webrequest_test"]; + +Components.utils.importGlobalProperties(["fetch", "XMLHttpRequest"]); + +this.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 000000000..dcffd0857 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/webrequest_worker.js @@ -0,0 +1,3 @@ +"use strict"; + +fetch("https://example.com/example.txt"); diff --git a/toolkit/components/extensions/test/xpcshell/.eslintrc.js b/toolkit/components/extensions/test/xpcshell/.eslintrc.js new file mode 100644 index 000000000..3758537ef --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/.eslintrc.js @@ -0,0 +1,9 @@ +"use strict"; + +module.exports = { // eslint-disable-line no-undef + "extends": "../../../../../testing/xpcshell/xpcshell.eslintrc.js", + + "globals": { + "browser": false, + }, +}; diff --git a/toolkit/components/extensions/test/xpcshell/data/file_download.html b/toolkit/components/extensions/test/xpcshell/data/file_download.html new file mode 100644 index 000000000..d970c6325 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_download.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> + +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + +<div>Download HTML File</div> + +</body> +</html> diff --git a/toolkit/components/extensions/test/xpcshell/data/file_download.txt b/toolkit/components/extensions/test/xpcshell/data/file_download.txt new file mode 100644 index 000000000..6293c7af7 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_download.txt @@ -0,0 +1 @@ +This is a sample file used in download tests. diff --git a/toolkit/components/extensions/test/xpcshell/head.js b/toolkit/components/extensions/test/xpcshell/head.js new file mode 100644 index 000000000..9e22be6da --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/head.js @@ -0,0 +1,111 @@ +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +/* exported createHttpServer, promiseConsoleOutput, cleanupDir */ + +Components.utils.import("resource://gre/modules/Task.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Timer.jsm"); +Components.utils.import("resource://testing-common/AddonTestUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", + "resource://gre/modules/AppConstants.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Extension", + "resource://gre/modules/Extension.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData", + "resource://gre/modules/Extension.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement", + "resource://gre/modules/ExtensionManagement.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestUtils", + "resource://testing-common/ExtensionXPCShellUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "HttpServer", + "resource://testing-common/httpd.js"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Schemas", + "resource://gre/modules/Schemas.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +ExtensionTestUtils.init(this); + +/** + * Creates a new HttpServer for testing, and begins listening on the + * specified port. Automatically shuts down the server when the test + * unit ends. + * + * @param {integer} [port] + * The port to listen on. If omitted, listen on a random + * port. The latter is the preferred behavior. + * + * @returns {HttpServer} + */ +function createHttpServer(port = -1) { + let server = new HttpServer(); + server.start(port); + + do_register_cleanup(() => { + return new Promise(resolve => { + server.stop(resolve); + }); + }); + + return server; +} + +var promiseConsoleOutput = Task.async(function* (task) { + const DONE = `=== console listener ${Math.random()} done ===`; + + let listener; + let messages = []; + let awaitListener = new Promise(resolve => { + listener = msg => { + if (msg == DONE) { + resolve(); + } else { + void (msg instanceof Ci.nsIConsoleMessage); + messages.push(msg); + } + }; + }); + + Services.console.registerListener(listener); + try { + let result = yield task(); + + Services.console.logStringMessage(DONE); + yield awaitListener; + + return {messages, result}; + } finally { + Services.console.unregisterListener(listener); + } +}); + +// Attempt to remove a directory. If the Windows OS is still using the +// file sometimes remove() will fail. So try repeatedly until we can +// remove it or we give up. +function cleanupDir(dir) { + let count = 0; + return new Promise((resolve, reject) => { + function tryToRemoveDir() { + count += 1; + try { + dir.remove(true); + } catch (e) { + // ignore + } + if (!dir.exists()) { + return resolve(); + } + if (count >= 25) { + return reject(`Failed to cleanup directory: ${dir}`); + } + setTimeout(tryToRemoveDir, 100); + } + tryToRemoveDir(); + }); +} diff --git a/toolkit/components/extensions/test/xpcshell/head_native_messaging.js b/toolkit/components/extensions/test/xpcshell/head_native_messaging.js new file mode 100644 index 000000000..f7c619b76 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/head_native_messaging.js @@ -0,0 +1,131 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* globals AppConstants, FileUtils */ +/* exported getSubprocessCount, setupHosts, waitForSubprocessExit */ + +XPCOMUtils.defineLazyModuleGetter(this, "MockRegistry", + "resource://testing-common/MockRegistry.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "setTimeout", + "resource://gre/modules/Timer.jsm"); + +let {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm"); + + +// It's important that we use a space in this directory name to make sure we +// correctly handle executing batch files with spaces in their path. +let tmpDir = FileUtils.getDir("TmpD", ["Native Messaging"]); +tmpDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + +do_register_cleanup(() => { + tmpDir.remove(true); +}); + +function getPath(filename) { + return OS.Path.join(tmpDir.path, filename); +} + +const ID = "native@tests.mozilla.org"; + + +function* setupHosts(scripts) { + const PERMS = {unixMode: 0o755}; + + const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment); + const pythonPath = yield Subprocess.pathSearch(env.get("PYTHON")); + + function* writeManifest(script, scriptPath, path) { + let body = `#!${pythonPath} -u\n${script.script}`; + + yield OS.File.writeAtomic(scriptPath, body); + yield OS.File.setPermissions(scriptPath, PERMS); + + let manifest = { + name: script.name, + description: script.description, + path, + type: "stdio", + allowed_extensions: [ID], + }; + + let manifestPath = getPath(`${script.name}.json`); + yield OS.File.writeAtomic(manifestPath, JSON.stringify(manifest)); + + return manifestPath; + } + + switch (AppConstants.platform) { + case "macosx": + case "linux": + let dirProvider = { + getFile(property) { + if (property == "XREUserNativeMessaging") { + return tmpDir.clone(); + } else if (property == "XRESysNativeMessaging") { + return tmpDir.clone(); + } + return null; + }, + }; + + Services.dirsvc.registerProvider(dirProvider); + do_register_cleanup(() => { + Services.dirsvc.unregisterProvider(dirProvider); + }); + + for (let script of scripts) { + let path = getPath(`${script.name}.py`); + + yield writeManifest(script, path, path); + } + break; + + case "win": + const REGKEY = String.raw`Software\Mozilla\NativeMessagingHosts`; + + let registry = new MockRegistry(); + do_register_cleanup(() => { + registry.shutdown(); + }); + + for (let script of scripts) { + // It's important that we use a space in this filename. See directory + // name comment above. + let batPath = getPath(`batch ${script.name}.bat`); + let scriptPath = getPath(`${script.name}.py`); + + let batBody = `@ECHO OFF\n${pythonPath} -u "${scriptPath}" %*\n`; + yield OS.File.writeAtomic(batPath, batBody); + + // Create absolute and relative path versions of the entry. + for (let [name, path] of [[script.name, batPath], + [`relative.${script.name}`, OS.Path.basename(batPath)]]) { + script.name = name; + let manifestPath = yield writeManifest(script, scriptPath, path); + + registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + `${REGKEY}\\${script.name}`, "", manifestPath); + } + } + break; + + default: + ok(false, `Native messaging is not supported on ${AppConstants.platform}`); + } +} + + +function getSubprocessCount() { + return SubprocessImpl.Process.getWorker().call("getProcesses", []) + .then(result => result.size); +} +function waitForSubprocessExit() { + return SubprocessImpl.Process.getWorker().call("waitForNoProcesses", []).then(() => { + // Return to the main event loop to give IO handlers enough time to consume + // their remaining buffered input. + return new Promise(resolve => setTimeout(resolve, 0)); + }); +} diff --git a/toolkit/components/extensions/test/xpcshell/head_sync.js b/toolkit/components/extensions/test/xpcshell/head_sync.js new file mode 100644 index 000000000..9b66b78e7 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/head_sync.js @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* exported withSyncContext */ + +Components.utils.import("resource://gre/modules/Services.jsm", this); +Components.utils.import("resource://gre/modules/ExtensionCommon.jsm", this); + +var { + BaseContext, +} = ExtensionCommon; + +class Context extends BaseContext { + constructor(principal) { + super(); + Object.defineProperty(this, "principal", { + value: principal, + configurable: true, + }); + this.sandbox = Components.utils.Sandbox(principal, {wantXrays: false}); + this.extension = {id: "test@web.extension"}; + } + + get cloneScope() { + return this.sandbox; + } +} + +/** + * Call the given function with a newly-constructed context. + * Unload the context on the way out. + * + * @param {function} f the function to call + */ +function* withContext(f) { + const ssm = Services.scriptSecurityManager; + const PRINCIPAL1 = ssm.createCodebasePrincipalFromOrigin("http://www.example.org"); + const context = new Context(PRINCIPAL1); + try { + yield* f(context); + } finally { + yield context.unload(); + } +} + +/** + * Like withContext(), but also turn on the "storage.sync" pref for + * the duration of the function. + * Calls to this function can be replaced with calls to withContext + * once the pref becomes on by default. + * + * @param {function} f the function to call + */ +function* withSyncContext(f) { + const STORAGE_SYNC_PREF = "webextensions.storage.sync.enabled"; + let prefs = Services.prefs; + + try { + prefs.setBoolPref(STORAGE_SYNC_PREF, true); + yield* withContext(f); + } finally { + prefs.clearUserPref(STORAGE_SYNC_PREF); + } +} diff --git a/toolkit/components/extensions/test/xpcshell/native_messaging.ini b/toolkit/components/extensions/test/xpcshell/native_messaging.ini new file mode 100644 index 000000000..d0e1da163 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/native_messaging.ini @@ -0,0 +1,13 @@ +[DEFAULT] +head = head.js head_native_messaging.js +tail = +firefox-appdir = browser +skip-if = appname == "thunderbird" || os == "android" +subprocess = true +support-files = + data/** +tags = webextensions + +[test_ext_native_messaging.js] +[test_ext_native_messaging_perf.js] +[test_ext_native_messaging_unresponsive.js] diff --git a/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js b/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js new file mode 100644 index 000000000..b6213baac --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js @@ -0,0 +1,38 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +Cu.import("resource://gre/modules/Preferences.jsm"); + +const ADDON_ID = "test@web.extension"; + +const aps = Cc["@mozilla.org/addons/policy-service;1"] + .getService(Ci.nsIAddonPolicyService).wrappedJSObject; + +do_register_cleanup(() => { + aps.setAddonCSP(ADDON_ID, null); +}); + +add_task(function* test_addon_csp() { + equal(aps.baseCSP, Preferences.get("extensions.webextensions.base-content-security-policy"), + "Expected base CSP value"); + + equal(aps.defaultCSP, Preferences.get("extensions.webextensions.default-content-security-policy"), + "Expected default CSP value"); + + equal(aps.getAddonCSP(ADDON_ID), aps.defaultCSP, + "CSP for unknown add-on ID should be the default CSP"); + + + const CUSTOM_POLICY = "script-src: 'self' https://xpcshell.test.custom.csp; object-src: 'none'"; + + aps.setAddonCSP(ADDON_ID, CUSTOM_POLICY); + + equal(aps.getAddonCSP(ADDON_ID), CUSTOM_POLICY, "CSP should point to add-on's custom policy"); + + + aps.setAddonCSP(ADDON_ID, null); + + equal(aps.getAddonCSP(ADDON_ID), aps.defaultCSP, + "CSP should revert to default when set to null"); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_csp_validator.js b/toolkit/components/extensions/test/xpcshell/test_csp_validator.js new file mode 100644 index 000000000..59a7322bc --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_csp_validator.js @@ -0,0 +1,85 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const cps = Cc["@mozilla.org/addons/content-policy;1"].getService(Ci.nsIAddonContentPolicy); + +add_task(function* test_csp_validator() { + let checkPolicy = (policy, expectedResult, message = null) => { + do_print(`Checking policy: ${policy}`); + + let result = cps.validateAddonCSP(policy); + equal(result, expectedResult); + }; + + checkPolicy("script-src 'self'; object-src 'self';", + null); + + let hash = "'sha256-NjZhMDQ1YjQ1MjEwMmM1OWQ4NDBlYzA5N2Q1OWQ5NDY3ZTEzYTNmMzRmNjQ5NGU1MzlmZmQzMmMxYmIzNWYxOCAgLQo='"; + + checkPolicy(`script-src 'self' https://com https://*.example.com moz-extension://09abcdef blob: filesystem: ${hash} 'unsafe-eval'; ` + + `object-src 'self' https://com https://*.example.com moz-extension://09abcdef blob: filesystem: ${hash}`, + null); + + checkPolicy("", + "Policy is missing a required \u2018script-src\u2019 directive"); + + checkPolicy("object-src 'none';", + "Policy is missing a required \u2018script-src\u2019 directive"); + + + checkPolicy("default-src 'self'", null, + "A valid default-src should count as a valid script-src or object-src"); + + checkPolicy("default-src 'self'; script-src 'self'", null, + "A valid default-src should count as a valid script-src or object-src"); + + checkPolicy("default-src 'self'; object-src 'self'", null, + "A valid default-src should count as a valid script-src or object-src"); + + + checkPolicy("default-src 'self'; script-src http://example.com", + "\u2018script-src\u2019 directive contains a forbidden http: protocol source", + "A valid default-src should not allow an invalid script-src directive"); + + checkPolicy("default-src 'self'; object-src http://example.com", + "\u2018object-src\u2019 directive contains a forbidden http: protocol source", + "A valid default-src should not allow an invalid object-src directive"); + + + checkPolicy("script-src 'self';", + "Policy is missing a required \u2018object-src\u2019 directive"); + + checkPolicy("script-src 'none'; object-src 'none'", + "\u2018script-src\u2019 must include the source 'self'"); + + checkPolicy("script-src 'self'; object-src 'none';", + null); + + checkPolicy("script-src 'self' 'unsafe-inline'; object-src 'self';", + "\u2018script-src\u2019 directive contains a forbidden 'unsafe-inline' keyword"); + + + let directives = ["script-src", "object-src"]; + + for (let [directive, other] of [directives, directives.slice().reverse()]) { + for (let src of ["https://*", "https://*.blogspot.com", "https://*"]) { + checkPolicy(`${directive} 'self' ${src}; ${other} 'self';`, + `https: wildcard sources in \u2018${directive}\u2019 directives must include at least one non-generic sub-domain (e.g., *.example.com rather than *.com)`); + } + + checkPolicy(`${directive} 'self' https:; ${other} 'self';`, + `https: protocol requires a host in \u2018${directive}\u2019 directives`); + + checkPolicy(`${directive} 'self' http://example.com; ${other} 'self';`, + `\u2018${directive}\u2019 directive contains a forbidden http: protocol source`); + + for (let protocol of ["http", "ftp", "meh"]) { + checkPolicy(`${directive} 'self' ${protocol}:; ${other} 'self';`, + `\u2018${directive}\u2019 directive contains a forbidden ${protocol}: protocol source`); + } + + checkPolicy(`${directive} 'self' 'nonce-01234'; ${other} 'self';`, + `\u2018${directive}\u2019 directive contains a forbidden 'nonce-*' keyword`); + } +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_alarms.js b/toolkit/components/extensions/test/xpcshell/test_ext_alarms.js new file mode 100644 index 000000000..936c984c6 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms.js @@ -0,0 +1,210 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_alarm_without_permissions() { + function backgroundScript() { + browser.test.assertTrue(!browser.alarms, + "alarm API is not available when the alarm permission is not required"); + browser.test.notifyPass("alarms_permission"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: [], + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("alarms_permission"); + yield extension.unload(); +}); + + +add_task(function* test_alarm_fires() { + function backgroundScript() { + let ALARM_NAME = "test_ext_alarms"; + let timer; + + browser.alarms.onAlarm.addListener(alarm => { + browser.test.assertEq(ALARM_NAME, alarm.name, "alarm has the correct name"); + clearTimeout(timer); + browser.test.notifyPass("alarm-fires"); + }); + + browser.alarms.create(ALARM_NAME, {delayInMinutes: 0.02}); + + timer = setTimeout(async () => { + browser.test.fail("alarm fired within expected time"); + let wasCleared = await browser.alarms.clear(ALARM_NAME); + browser.test.assertTrue(wasCleared, "alarm was cleared"); + browser.test.notifyFail("alarm-fires"); + }, 10000); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: ["alarms"], + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("alarm-fires"); + yield extension.unload(); +}); + + +add_task(function* test_alarm_fires_with_when() { + function backgroundScript() { + let ALARM_NAME = "test_ext_alarms"; + let timer; + + browser.alarms.onAlarm.addListener(alarm => { + browser.test.assertEq(ALARM_NAME, alarm.name, "alarm has the expected name"); + clearTimeout(timer); + browser.test.notifyPass("alarm-when"); + }); + + browser.alarms.create(ALARM_NAME, {when: Date.now() + 1000}); + + timer = setTimeout(async () => { + browser.test.fail("alarm fired within expected time"); + let wasCleared = await browser.alarms.clear(ALARM_NAME); + browser.test.assertTrue(wasCleared, "alarm was cleared"); + browser.test.notifyFail("alarm-when"); + }, 10000); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: ["alarms"], + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("alarm-when"); + yield extension.unload(); +}); + + +add_task(function* test_alarm_clear_non_matching_name() { + async function backgroundScript() { + let ALARM_NAME = "test_ext_alarms"; + + browser.alarms.create(ALARM_NAME, {when: Date.now() + 2000}); + + let wasCleared = await browser.alarms.clear(ALARM_NAME + "1"); + browser.test.assertFalse(wasCleared, "alarm was not cleared"); + + let alarms = await browser.alarms.getAll(); + browser.test.assertEq(1, alarms.length, "alarm was not removed"); + browser.test.notifyPass("alarm-clear"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: ["alarms"], + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("alarm-clear"); + yield extension.unload(); +}); + + +add_task(function* test_alarm_get_and_clear_single_argument() { + async function backgroundScript() { + browser.alarms.create({when: Date.now() + 2000}); + + let alarm = await browser.alarms.get(); + browser.test.assertEq("", alarm.name, "expected alarm returned"); + + let wasCleared = await browser.alarms.clear(); + browser.test.assertTrue(wasCleared, "alarm was cleared"); + + let alarms = await browser.alarms.getAll(); + browser.test.assertEq(0, alarms.length, "alarm was removed"); + + browser.test.notifyPass("alarm-single-arg"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: ["alarms"], + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("alarm-single-arg"); + yield extension.unload(); +}); + + +add_task(function* test_get_get_all_clear_all_alarms() { + async function backgroundScript() { + const ALARM_NAME = "test_alarm"; + + let suffixes = [0, 1, 2]; + + for (let suffix of suffixes) { + browser.alarms.create(ALARM_NAME + suffix, {when: Date.now() + (suffix + 1) * 10000}); + } + + let alarms = await browser.alarms.getAll(); + browser.test.assertEq(suffixes.length, alarms.length, "expected number of alarms were found"); + alarms.forEach((alarm, index) => { + browser.test.assertEq(ALARM_NAME + index, alarm.name, "alarm has the expected name"); + }); + + + for (let suffix of suffixes) { + let alarm = await browser.alarms.get(ALARM_NAME + suffix); + browser.test.assertEq(ALARM_NAME + suffix, alarm.name, "alarm has the expected name"); + browser.test.sendMessage(`get-${suffix}`); + } + + let wasCleared = await browser.alarms.clear(ALARM_NAME + suffixes[0]); + browser.test.assertTrue(wasCleared, "alarm was cleared"); + + alarms = await browser.alarms.getAll(); + browser.test.assertEq(2, alarms.length, "alarm was removed"); + + let alarm = await browser.alarms.get(ALARM_NAME + suffixes[0]); + browser.test.assertEq(undefined, alarm, "non-existent alarm is undefined"); + browser.test.sendMessage(`get-invalid`); + + wasCleared = await browser.alarms.clearAll(); + browser.test.assertTrue(wasCleared, "alarms were cleared"); + + alarms = await browser.alarms.getAll(); + browser.test.assertEq(0, alarms.length, "no alarms exist"); + browser.test.sendMessage("clearAll"); + browser.test.sendMessage("clear"); + browser.test.sendMessage("getAll"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: ["alarms"], + }, + }); + + yield Promise.all([ + extension.startup(), + extension.awaitMessage("getAll"), + extension.awaitMessage("get-0"), + extension.awaitMessage("get-1"), + extension.awaitMessage("get-2"), + extension.awaitMessage("clear"), + extension.awaitMessage("get-invalid"), + extension.awaitMessage("clearAll"), + ]); + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_alarms_does_not_fire.js b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_does_not_fire.js new file mode 100644 index 000000000..11407b108 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_does_not_fire.js @@ -0,0 +1,33 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_cleared_alarm_does_not_fire() { + async function backgroundScript() { + let ALARM_NAME = "test_ext_alarms"; + + browser.alarms.onAlarm.addListener(alarm => { + browser.test.fail("cleared alarm does not fire"); + browser.test.notifyFail("alarm-cleared"); + }); + browser.alarms.create(ALARM_NAME, {when: Date.now() + 1000}); + + let wasCleared = await browser.alarms.clear(ALARM_NAME); + browser.test.assertTrue(wasCleared, "alarm was cleared"); + + await new Promise(resolve => setTimeout(resolve, 2000)); + + browser.test.notifyPass("alarm-cleared"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: ["alarms"], + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("alarm-cleared"); + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_alarms_periodic.js b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_periodic.js new file mode 100644 index 000000000..6bcdf4e33 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_periodic.js @@ -0,0 +1,44 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_periodic_alarm_fires() { + function backgroundScript() { + const ALARM_NAME = "test_ext_alarms"; + let count = 0; + let timer; + + browser.alarms.onAlarm.addListener(async alarm => { + browser.test.assertEq(alarm.name, ALARM_NAME, "alarm has the expected name"); + if (count++ === 3) { + clearTimeout(timer); + let wasCleared = await browser.alarms.clear(ALARM_NAME); + browser.test.assertTrue(wasCleared, "alarm was cleared"); + + browser.test.notifyPass("alarm-periodic"); + } + }); + + browser.alarms.create(ALARM_NAME, {periodInMinutes: 0.02}); + + timer = setTimeout(async () => { + browser.test.fail("alarm fired expected number of times"); + + let wasCleared = await browser.alarms.clear(ALARM_NAME); + browser.test.assertTrue(wasCleared, "alarm was cleared"); + + browser.test.notifyFail("alarm-periodic"); + }, 30000); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: ["alarms"], + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("alarm-periodic"); + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_alarms_replaces.js b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_replaces.js new file mode 100644 index 000000000..96f61acb5 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_replaces.js @@ -0,0 +1,44 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + + +add_task(function* test_duplicate_alarm_name_replaces_alarm() { + function backgroundScript() { + let count = 0; + + browser.alarms.onAlarm.addListener(async alarm => { + if (alarm.name === "master alarm") { + browser.alarms.create("child alarm", {delayInMinutes: 0.05}); + let results = await browser.alarms.getAll(); + + browser.test.assertEq(2, results.length, "exactly two alarms exist"); + browser.test.assertEq("master alarm", results[0].name, "first alarm has the expected name"); + browser.test.assertEq("child alarm", results[1].name, "second alarm has the expected name"); + + if (count++ === 3) { + await browser.alarms.clear("master alarm"); + await browser.alarms.clear("child alarm"); + + browser.test.notifyPass("alarm-duplicate"); + } + } else { + browser.test.fail("duplicate named alarm replaced existing alarm"); + browser.test.notifyFail("alarm-duplicate"); + } + }); + + browser.alarms.create("master alarm", {delayInMinutes: 0.025, periodInMinutes: 0.025}); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: ["alarms"], + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("alarm-duplicate"); + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js b/toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js new file mode 100644 index 000000000..d653d0e7a --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js @@ -0,0 +1,64 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +let {Management} = Cu.import("resource://gre/modules/Extension.jsm", {}); +function getNextContext() { + return new Promise(resolve => { + Management.on("proxy-context-load", function listener(type, context) { + Management.off("proxy-context-load", listener); + resolve(context); + }); + }); +} + +add_task(function* test_storage_api_without_permissions() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + // Force API initialization. + void browser.storage; + }, + + manifest: { + permissions: [], + }, + }); + + let contextPromise = getNextContext(); + yield extension.startup(); + + let context = yield contextPromise; + + // Force API initialization. + void context.apiObj; + + ok(!("storage" in context.apiObj), + "The storage API should not be initialized"); + + yield extension.unload(); +}); + +add_task(function* test_storage_api_with_permissions() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + void browser.storage; + }, + + manifest: { + permissions: ["storage"], + }, + }); + + let contextPromise = getNextContext(); + yield extension.startup(); + + let context = yield contextPromise; + + // Force API initialization. + void context.apiObj; + + equal(typeof context.apiObj.storage, "object", + "The storage API should be initialized"); + + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js b/toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js new file mode 100644 index 000000000..3f6672a11 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +Cu.import("resource://gre/modules/ExtensionCommon.jsm"); + +const { + SchemaAPIManager, +} = ExtensionCommon; + +this.unknownvar = "Some module-global var"; + +var gUniqueId = 0; + +// SchemaAPIManager's loadScript uses loadSubScript to load a script. This +// requires a local (resource://) URL. So create such a temporary URL for +// testing. +function toLocalURI(code) { + let dataUrl = `data:charset=utf-8,${encodeURIComponent(code)}`; + let uniqueResPart = `need-a-local-uri-for-subscript-loading-${++gUniqueId}`; + Services.io.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler) + .setSubstitution(uniqueResPart, Services.io.newURI(dataUrl, null, null)); + return `resource://${uniqueResPart}`; +} + +add_task(function* test_global_isolation() { + let manA = new SchemaAPIManager("procA"); + let manB = new SchemaAPIManager("procB"); + + // The "global" variable should be persistent and shared. + manA.loadScript(toLocalURI`global.globalVar = 1;`); + do_check_eq(manA.global.globalVar, 1); + do_check_eq(manA.global.unknownvar, undefined); + manA.loadScript(toLocalURI`global.canSeeGlobal = global.globalVar;`); + do_check_eq(manA.global.canSeeGlobal, 1); + + // Each loadScript call should have their own scope, and global is shared. + manA.loadScript(toLocalURI`this.aVar = 1; global.thisScopeVar = aVar`); + do_check_eq(manA.global.aVar, undefined); + do_check_eq(manA.global.thisScopeVar, 1); + manA.loadScript(toLocalURI`global.differentScopeVar = this.aVar;`); + do_check_eq(manA.global.differentScopeVar, undefined); + manA.loadScript(toLocalURI`global.cantSeeOtherScope = typeof aVar;`); + do_check_eq(manA.global.cantSeeOtherScope, "undefined"); + + manB.loadScript(toLocalURI`global.tryReadOtherGlobal = global.tryagain;`); + do_check_eq(manA.global.tryReadOtherGlobal, undefined); + + // Cu.import without second argument exports to the caller's global. Let's + // verify that it does not leak to the SchemaAPIManager's global. + do_check_eq(typeof ExtensionUtils, "undefined"); // Sanity check #1. + manA.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`); + do_check_eq(manA.global.hasExtUtils, "undefined"); // Sanity check #2 + + Cu.import("resource://gre/modules/ExtensionUtils.jsm"); + do_check_eq(typeof ExtensionUtils, "object"); // Sanity check #3. + + manA.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`); + do_check_eq(manA.global.hasExtUtils, "undefined"); + manB.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`); + do_check_eq(manB.global.hasExtUtils, "undefined"); + + // Confirm that Cu.import does not leak between SchemaAPIManager globals. + manA.loadScript(toLocalURI` + Cu.import("resource://gre/modules/ExtensionUtils.jsm"); + global.hasExtUtils = typeof ExtensionUtils; + `); + do_check_eq(manA.global.hasExtUtils, "object"); // Sanity check. + manB.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`); + do_check_eq(manB.global.hasExtUtils, "undefined"); + + // Prototype modifications should be isolated. + manA.loadScript(toLocalURI` + Object.prototype.modifiedByA = "Prrft"; + global.fromA = {}; + `); + manA.loadScript(toLocalURI` + global.fromAagain = {}; + `); + manB.loadScript(toLocalURI` + global.fromB = {}; + `); + do_check_eq(manA.global.modifiedByA, "Prrft"); + do_check_eq(manA.global.fromA.modifiedByA, "Prrft"); + do_check_eq(manA.global.fromAagain.modifiedByA, "Prrft"); + do_check_eq(manB.global.modifiedByA, undefined); + do_check_eq(manB.global.fromB.modifiedByA, undefined); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_load_events.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_load_events.js new file mode 100644 index 000000000..26282fcb9 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_load_events.js @@ -0,0 +1,23 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* eslint-disable mozilla/balanced-listeners */ + +add_task(function* test_DOMContentLoaded_in_generated_background_page() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + function reportListener(event) { + browser.test.sendMessage("eventname", event.type); + } + document.addEventListener("DOMContentLoaded", reportListener); + window.addEventListener("load", reportListener); + }, + }); + + yield extension.startup(); + equal("DOMContentLoaded", yield extension.awaitMessage("eventname")); + equal("load", yield extension.awaitMessage("eventname")); + + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_reload.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_reload.js new file mode 100644 index 000000000..4bf59b798 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_reload.js @@ -0,0 +1,24 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_reload_generated_background_page() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + if (location.hash !== "#firstrun") { + browser.test.sendMessage("first run"); + location.hash = "#firstrun"; + browser.test.assertEq("#firstrun", location.hash); + location.reload(); + } else { + browser.test.notifyPass("second run"); + } + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("first run"); + yield extension.awaitFinish("second run"); + + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_global_history.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_global_history.js new file mode 100644 index 000000000..092a9f5b3 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_global_history.js @@ -0,0 +1,22 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +Cu.import("resource://testing-common/PlacesTestUtils.jsm"); + +add_task(function* test_global_history() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.test.sendMessage("background-loaded", location.href); + }, + }); + + yield extension.startup(); + + let backgroundURL = yield extension.awaitMessage("background-loaded"); + + yield extension.unload(); + + let exists = yield PlacesTestUtils.isPageInDB(backgroundURL); + ok(!exists, "Background URL should not be in history database"); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_private_browsing.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_private_browsing.js new file mode 100644 index 000000000..8e8b5e0b0 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_private_browsing.js @@ -0,0 +1,40 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +Cu.import("resource://gre/modules/Preferences.jsm"); + +function* testBackgroundPage(expected) { + let extension = ExtensionTestUtils.loadExtension({ + async background() { + browser.test.assertEq(window, browser.extension.getBackgroundPage(), + "Caller should be able to access itself as a background page"); + browser.test.assertEq(window, await browser.runtime.getBackgroundPage(), + "Caller should be able to access itself as a background page"); + + browser.test.sendMessage("incognito", browser.extension.inIncognitoContext); + }, + }); + + yield extension.startup(); + + let incognito = yield extension.awaitMessage("incognito"); + equal(incognito, expected.incognito, "Expected incognito value"); + + yield extension.unload(); +} + +add_task(function* test_background_incognito() { + do_print("Test background page incognito value with permanent private browsing disabled"); + + yield testBackgroundPage({incognito: false}); + + do_print("Test background page incognito value with permanent private browsing enabled"); + + Preferences.set("browser.privatebrowsing.autostart", true); + do_register_cleanup(() => { + Preferences.reset("browser.privatebrowsing.autostart"); + }); + + yield testBackgroundPage({incognito: true}); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_runtime_connect_params.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_runtime_connect_params.js new file mode 100644 index 000000000..426833edd --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_runtime_connect_params.js @@ -0,0 +1,72 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function backgroundScript() { + let received_ports_number = 0; + + const expected_received_ports_number = 1; + + function countReceivedPorts(port) { + received_ports_number++; + + if (port.name == "check-results") { + browser.runtime.onConnect.removeListener(countReceivedPorts); + + browser.test.assertEq(expected_received_ports_number, received_ports_number, "invalid connect should not create a port"); + + browser.test.notifyPass("runtime.connect invalid params"); + } + } + + browser.runtime.onConnect.addListener(countReceivedPorts); + + let childFrame = document.createElement("iframe"); + childFrame.src = "extensionpage.html"; + document.body.appendChild(childFrame); +} + +function senderScript() { + let detected_invalid_connect_params = 0; + + const invalid_connect_params = [ + // too many params + ["fake-extensions-id", {name: "fake-conn-name"}, "unexpected third params"], + // invalid params format + [{}, {}], + ["fake-extensions-id", "invalid-connect-info-format"], + ]; + const expected_detected_invalid_connect_params = invalid_connect_params.length; + + function assertInvalidConnectParamsException(params) { + try { + browser.runtime.connect(...params); + } catch (e) { + detected_invalid_connect_params++; + browser.test.assertTrue(e.toString().indexOf("Incorrect argument types for runtime.connect.") >= 0, "exception message is correct"); + } + } + for (let params of invalid_connect_params) { + assertInvalidConnectParamsException(params); + } + browser.test.assertEq(expected_detected_invalid_connect_params, detected_invalid_connect_params, "all invalid runtime.connect params detected"); + + browser.runtime.connect(browser.runtime.id, {name: "check-results"}); +} + +let extensionData = { + background: backgroundScript, + files: { + "senderScript.js": senderScript, + "extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="senderScript.js"></script>`, + }, +}; + +add_task(function* test_backgroundRuntimeConnectParams() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + yield extension.awaitFinish("runtime.connect invalid params"); + + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_sub_windows.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_sub_windows.js new file mode 100644 index 000000000..c5f2f1332 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_sub_windows.js @@ -0,0 +1,45 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testBackgroundWindow() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.test.log("background script executed"); + + browser.test.sendMessage("background-script-load"); + + let img = document.createElement("img"); + img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; + document.body.appendChild(img); + + img.onload = () => { + browser.test.log("image loaded"); + + let iframe = document.createElement("iframe"); + iframe.src = "about:blank?1"; + + iframe.onload = () => { + browser.test.log("iframe loaded"); + setTimeout(() => { + browser.test.notifyPass("background sub-window test done"); + }, 0); + }; + document.body.appendChild(iframe); + }; + }, + }); + + let loadCount = 0; + extension.onMessage("background-script-load", () => { + loadCount++; + }); + + yield extension.startup(); + + yield extension.awaitFinish("background sub-window test done"); + + equal(loadCount, 1, "background script loaded only once"); + + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_window_properties.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_window_properties.js new file mode 100644 index 000000000..948e2913e --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_window_properties.js @@ -0,0 +1,34 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* testBackgroundWindowProperties() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + let expectedValues = { + screenX: 0, + screenY: 0, + outerWidth: 0, + outerHeight: 0, + }; + + for (let k in window) { + try { + if (k in expectedValues) { + browser.test.assertEq(expectedValues[k], window[k], + `should return the expected value for window property: ${k}`); + } else { + void window[k]; + } + } catch (e) { + browser.test.assertEq(null, e, `unexpected exception accessing window property: ${k}`); + } + } + + browser.test.notifyPass("background.testWindowProperties.done"); + }, + }); + yield extension.startup(); + yield extension.awaitFinish("background.testWindowProperties.done"); + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js b/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js new file mode 100644 index 000000000..56a14e189 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js @@ -0,0 +1,190 @@ +"use strict"; + +const global = this; + +Cu.import("resource://gre/modules/Timer.jsm"); + +Cu.import("resource://gre/modules/ExtensionCommon.jsm"); +Cu.import("resource://gre/modules/ExtensionUtils.jsm"); + +var { + BaseContext, +} = ExtensionCommon; + +var { + EventManager, + SingletonEventManager, +} = ExtensionUtils; + +class StubContext extends BaseContext { + constructor() { + let fakeExtension = {id: "test@web.extension"}; + super("testEnv", fakeExtension); + this.sandbox = Cu.Sandbox(global); + } + + get cloneScope() { + return this.sandbox; + } +} + + +add_task(function* test_post_unload_promises() { + let context = new StubContext(); + + let fail = result => { + ok(false, `Unexpected callback: ${result}`); + }; + + // Make sure promises resolve normally prior to unload. + let promises = [ + context.wrapPromise(Promise.resolve()), + context.wrapPromise(Promise.reject({message: ""})).catch(() => {}), + ]; + + yield Promise.all(promises); + + // Make sure promises that resolve after unload do not trigger + // resolution handlers. + + context.wrapPromise(Promise.resolve("resolved")) + .then(fail); + + context.wrapPromise(Promise.reject({message: "rejected"})) + .then(fail, fail); + + context.unload(); + + // The `setTimeout` ensures that we return to the event loop after + // promise resolution, which means we're guaranteed to return after + // any micro-tasks that get enqueued by the resolution handlers above. + yield new Promise(resolve => setTimeout(resolve, 0)); +}); + + +add_task(function* test_post_unload_listeners() { + let context = new StubContext(); + + let fireEvent; + let onEvent = new EventManager(context, "onEvent", fire => { + fireEvent = fire; + return () => {}; + }); + + let fireSingleton; + let onSingleton = new SingletonEventManager(context, "onSingleton", callback => { + fireSingleton = () => { + Promise.resolve().then(callback); + }; + return () => {}; + }); + + let fail = event => { + ok(false, `Unexpected event: ${event}`); + }; + + // Check that event listeners aren't called after they've been removed. + onEvent.addListener(fail); + onSingleton.addListener(fail); + + let promises = [ + new Promise(resolve => onEvent.addListener(resolve)), + new Promise(resolve => onSingleton.addListener(resolve)), + ]; + + fireEvent("onEvent"); + fireSingleton("onSingleton"); + + // Both `fireEvent` calls are dispatched asynchronously, so they won't + // have fired by this point. The `fail` listeners that we remove now + // should not be called, even though the events have already been + // enqueued. + onEvent.removeListener(fail); + onSingleton.removeListener(fail); + + // Wait for the remaining listeners to be called, which should always + // happen after the `fail` listeners would normally be called. + yield Promise.all(promises); + + // Check that event listeners aren't called after the context has + // unloaded. + onEvent.addListener(fail); + onSingleton.addListener(fail); + + // The EventManager `fire` callback always dispatches events + // asynchronously, so we need to test that any pending event callbacks + // aren't fired after the context unloads. We also need to test that + // any `fire` calls that happen *after* the context is unloaded also + // do not trigger callbacks. + fireEvent("onEvent"); + Promise.resolve("onEvent").then(fireEvent); + + fireSingleton("onSingleton"); + Promise.resolve("onSingleton").then(fireSingleton); + + context.unload(); + + // The `setTimeout` ensures that we return to the event loop after + // promise resolution, which means we're guaranteed to return after + // any micro-tasks that get enqueued by the resolution handlers above. + yield new Promise(resolve => setTimeout(resolve, 0)); +}); + +class Context extends BaseContext { + constructor(principal) { + let fakeExtension = {id: "test@web.extension"}; + super("testEnv", fakeExtension); + Object.defineProperty(this, "principal", { + value: principal, + configurable: true, + }); + this.sandbox = Cu.Sandbox(principal, {wantXrays: false}); + } + + get cloneScope() { + return this.sandbox; + } +} + +let ssm = Services.scriptSecurityManager; +const PRINCIPAL1 = ssm.createCodebasePrincipalFromOrigin("http://www.example.org"); +const PRINCIPAL2 = ssm.createCodebasePrincipalFromOrigin("http://www.somethingelse.org"); + +// Test that toJSON() works in the json sandbox +add_task(function* test_stringify_toJSON() { + let context = new Context(PRINCIPAL1); + let obj = Cu.evalInSandbox("({hidden: true, toJSON() { return {visible: true}; } })", context.sandbox); + + let stringified = context.jsonStringify(obj); + let expected = JSON.stringify({visible: true}); + equal(stringified, expected, "Stringified object with toJSON() method is as expected"); +}); + +// Test that stringifying in inaccessible property throws +add_task(function* test_stringify_inaccessible() { + let context = new Context(PRINCIPAL1); + let sandbox = context.sandbox; + let sandbox2 = Cu.Sandbox(PRINCIPAL2); + + Cu.waiveXrays(sandbox).subobj = Cu.evalInSandbox("({ subobject: true })", sandbox2); + let obj = Cu.evalInSandbox("({ local: true, nested: subobj })", sandbox); + Assert.throws(() => { + context.jsonStringify(obj); + }); +}); + +add_task(function* test_stringify_accessible() { + // Test that an accessible property from another global is included + let principal = Cu.getObjectPrincipal(Cu.Sandbox([PRINCIPAL1, PRINCIPAL2])); + let context = new Context(principal); + let sandbox = context.sandbox; + let sandbox2 = Cu.Sandbox(PRINCIPAL2); + + Cu.waiveXrays(sandbox).subobj = Cu.evalInSandbox("({ subobject: true })", sandbox2); + let obj = Cu.evalInSandbox("({ local: true, nested: subobj })", sandbox); + let stringified = context.jsonStringify(obj); + + let expected = JSON.stringify({local: true, nested: {subobject: true}}); + equal(stringified, expected, "Stringified object with accessible property is as expected"); +}); + diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads.js new file mode 100644 index 000000000..058b9b18c --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads.js @@ -0,0 +1,76 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_downloads_api_namespace_and_permissions() { + function backgroundScript() { + browser.test.assertTrue(!!browser.downloads, "`downloads` API is present."); + browser.test.assertTrue(!!browser.downloads.FilenameConflictAction, + "`downloads.FilenameConflictAction` enum is present."); + browser.test.assertTrue(!!browser.downloads.InterruptReason, + "`downloads.InterruptReason` enum is present."); + browser.test.assertTrue(!!browser.downloads.DangerType, + "`downloads.DangerType` enum is present."); + browser.test.assertTrue(!!browser.downloads.State, + "`downloads.State` enum is present."); + browser.test.notifyPass("downloads tests"); + } + + let extensionData = { + background: backgroundScript, + manifest: { + permissions: ["downloads", "downloads.open", "downloads.shelf"], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + yield extension.awaitFinish("downloads tests"); + yield extension.unload(); +}); + +add_task(function* test_downloads_open_permission() { + function backgroundScript() { + browser.test.assertFalse("open" in browser.downloads, + "`downloads.open` permission is required."); + browser.test.notifyPass("downloads tests"); + } + + let extensionData = { + background: backgroundScript, + manifest: { + permissions: ["downloads"], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + yield extension.awaitFinish("downloads tests"); + yield extension.unload(); +}); + +add_task(function* test_downloads_open() { + async function backgroundScript() { + await browser.test.assertRejects( + browser.downloads.open(10), + "Invalid download id 10", + "The error is informative."); + + browser.test.notifyPass("downloads tests"); + + // TODO: Once downloads.{pause,cancel,resume} lands (bug 1245602) test that this gives a good + // error when called with an incompleted download. + } + + let extensionData = { + background: backgroundScript, + manifest: { + permissions: ["downloads", "downloads.open"], + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + yield extension.awaitFinish("downloads tests"); + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js new file mode 100644 index 000000000..37ddd4d7c --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js @@ -0,0 +1,354 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* global OS */ + +Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/Downloads.jsm"); + +const gServer = createHttpServer(); +gServer.registerDirectory("/data/", do_get_file("data")); + +const WINDOWS = AppConstants.platform == "win"; + +const BASE = `http://localhost:${gServer.identity.primaryPort}/data`; +const FILE_NAME = "file_download.txt"; +const FILE_URL = BASE + "/" + FILE_NAME; +const FILE_NAME_UNIQUE = "file_download(1).txt"; +const FILE_LEN = 46; + +let downloadDir; + +function setup() { + downloadDir = FileUtils.getDir("TmpD", ["downloads"]); + downloadDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + do_print(`Using download directory ${downloadDir.path}`); + + Services.prefs.setIntPref("browser.download.folderList", 2); + Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, downloadDir); + + do_register_cleanup(() => { + Services.prefs.clearUserPref("browser.download.folderList"); + Services.prefs.clearUserPref("browser.download.dir"); + + let entries = downloadDir.directoryEntries; + while (entries.hasMoreElements()) { + let entry = entries.getNext().QueryInterface(Ci.nsIFile); + ok(false, `Leftover file ${entry.path} in download directory`); + entry.remove(false); + } + + downloadDir.remove(false); + }); +} + +function backgroundScript() { + let blobUrl; + browser.test.onMessage.addListener(async (msg, ...args) => { + if (msg == "download.request") { + let options = args[0]; + + if (options.blobme) { + let blob = new Blob(options.blobme); + delete options.blobme; + blobUrl = options.url = window.URL.createObjectURL(blob); + } + + try { + let id = await browser.downloads.download(options); + browser.test.sendMessage("download.done", {status: "success", id}); + } catch (error) { + browser.test.sendMessage("download.done", {status: "error", errmsg: error.message}); + } + } else if (msg == "killTheBlob") { + window.URL.revokeObjectURL(blobUrl); + blobUrl = null; + } + }); + + browser.test.sendMessage("ready"); +} + +// This function is a bit of a sledgehammer, it looks at every download +// the browser knows about and waits for all active downloads to complete. +// But we only start one at a time and only do a handful in total, so +// this lets us test download() without depending on anything else. +async function waitForDownloads() { + let list = await Downloads.getList(Downloads.ALL); + let downloads = await list.getAll(); + + let inprogress = downloads.filter(dl => !dl.stopped); + return Promise.all(inprogress.map(dl => dl.whenSucceeded())); +} + +// Create a file in the downloads directory. +function touch(filename) { + let file = downloadDir.clone(); + file.append(filename); + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); +} + +// Remove a file in the downloads directory. +function remove(filename, recursive = false) { + let file = downloadDir.clone(); + file.append(filename); + file.remove(recursive); +} + +add_task(function* test_downloads() { + setup(); + + let extension = ExtensionTestUtils.loadExtension({ + background: `(${backgroundScript})()`, + manifest: { + permissions: ["downloads"], + }, + }); + + function download(options) { + extension.sendMessage("download.request", options); + return extension.awaitMessage("download.done"); + } + + async function testDownload(options, localFile, expectedSize, description) { + let msg = await download(options); + equal(msg.status, "success", `downloads.download() works with ${description}`); + + await waitForDownloads(); + + let localPath = downloadDir.clone(); + let parts = Array.isArray(localFile) ? localFile : [localFile]; + + parts.map(p => localPath.append(p)); + equal(localPath.fileSize, expectedSize, "Downloaded file has expected size"); + localPath.remove(false); + } + + yield extension.startup(); + yield extension.awaitMessage("ready"); + do_print("extension started"); + + // Call download() with just the url property. + yield testDownload({url: FILE_URL}, FILE_NAME, FILE_LEN, "just source"); + + // Call download() with a filename property. + yield testDownload({ + url: FILE_URL, + filename: "newpath.txt", + }, "newpath.txt", FILE_LEN, "source and filename"); + + // Call download() with a filename with subdirs. + yield testDownload({ + url: FILE_URL, + filename: "sub/dir/file", + }, ["sub", "dir", "file"], FILE_LEN, "source and filename with subdirs"); + + // Call download() with a filename with existing subdirs. + yield testDownload({ + url: FILE_URL, + filename: "sub/dir/file2", + }, ["sub", "dir", "file2"], FILE_LEN, "source and filename with existing subdirs"); + + // Only run Windows path separator test on Windows. + if (WINDOWS) { + // Call download() with a filename with Windows path separator. + yield testDownload({ + url: FILE_URL, + filename: "sub\\dir\\file3", + }, ["sub", "dir", "file3"], FILE_LEN, "filename with Windows path separator"); + } + remove("sub", true); + + // Call download(), filename with subdir, skipping parts. + yield testDownload({ + url: FILE_URL, + filename: "skip//part", + }, ["skip", "part"], FILE_LEN, "source, filename, with subdir, skipping parts"); + remove("skip", true); + + // Check conflictAction of "uniquify". + touch(FILE_NAME); + yield testDownload({ + url: FILE_URL, + conflictAction: "uniquify", + }, FILE_NAME_UNIQUE, FILE_LEN, "conflictAction=uniquify"); + // todo check that preexisting file was not modified? + remove(FILE_NAME); + + // Check conflictAction of "overwrite". + touch(FILE_NAME); + yield testDownload({ + url: FILE_URL, + conflictAction: "overwrite", + }, FILE_NAME, FILE_LEN, "conflictAction=overwrite"); + + // Try to download in invalid url + yield download({url: "this is not a valid URL"}).then(msg => { + equal(msg.status, "error", "downloads.download() fails with invalid url"); + ok(/not a valid URL/.test(msg.errmsg), "error message for invalid url is correct"); + }); + + // Try to download to an empty path. + yield download({ + url: FILE_URL, + filename: "", + }).then(msg => { + equal(msg.status, "error", "downloads.download() fails with empty filename"); + equal(msg.errmsg, "filename must not be empty", "error message for empty filename is correct"); + }); + + // Try to download to an absolute path. + const absolutePath = OS.Path.join(WINDOWS ? "\\tmp" : "/tmp", "file_download.txt"); + yield download({ + url: FILE_URL, + filename: absolutePath, + }).then(msg => { + equal(msg.status, "error", "downloads.download() fails with absolute filename"); + equal(msg.errmsg, "filename must not be an absolute path", `error message for absolute path (${absolutePath}) is correct`); + }); + + if (WINDOWS) { + yield download({ + url: FILE_URL, + filename: "C:\\file_download.txt", + }).then(msg => { + equal(msg.status, "error", "downloads.download() fails with absolute filename"); + equal(msg.errmsg, "filename must not be an absolute path", "error message for absolute path with drive letter is correct"); + }); + } + + // Try to download to a relative path containing .. + yield download({ + url: FILE_URL, + filename: OS.Path.join("..", "file_download.txt"), + }).then(msg => { + equal(msg.status, "error", "downloads.download() fails with back-references"); + equal(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct"); + }); + + // Try to download to a long relative path containing .. + yield download({ + url: FILE_URL, + filename: OS.Path.join("foo", "..", "..", "file_download.txt"), + }).then(msg => { + equal(msg.status, "error", "downloads.download() fails with back-references"); + equal(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct"); + }); + + // Try to download a blob url + const BLOB_STRING = "Hello, world"; + yield testDownload({ + blobme: [BLOB_STRING], + filename: FILE_NAME, + }, FILE_NAME, BLOB_STRING.length, "blob url"); + extension.sendMessage("killTheBlob"); + + // Try to download a blob url without a given filename + yield testDownload({ + blobme: [BLOB_STRING], + }, "download", BLOB_STRING.length, "blob url with no filename"); + extension.sendMessage("killTheBlob"); + + yield extension.unload(); +}); + +add_task(function* test_download_post() { + const server = createHttpServer(); + const url = `http://localhost:${server.identity.primaryPort}/post-log`; + + let received; + server.registerPathHandler("/post-log", request => { + received = request; + }); + + // Confirm received vs. expected values. + function confirm(method, headers = {}, body) { + equal(received.method, method, "method is correct"); + + for (let name in headers) { + ok(received.hasHeader(name), `header ${name} received`); + equal(received.getHeader(name), headers[name], `header ${name} is correct`); + } + + if (body) { + const str = NetUtil.readInputStreamToString(received.bodyInputStream, + received.bodyInputStream.available()); + equal(str, body, "body is correct"); + } + } + + function background() { + browser.test.onMessage.addListener(async options => { + try { + await browser.downloads.download(options); + } catch (err) { + browser.test.sendMessage("done", {err: err.message}); + } + }); + browser.downloads.onChanged.addListener(({state}) => { + if (state && state.current === "complete") { + browser.test.sendMessage("done", {ok: true}); + } + }); + } + + const manifest = {permissions: ["downloads"]}; + const extension = ExtensionTestUtils.loadExtension({background, manifest}); + yield extension.startup(); + + function download(options) { + options.url = url; + options.conflictAction = "overwrite"; + + extension.sendMessage(options); + return extension.awaitMessage("done"); + } + + // Test method option. + let result = yield download({}); + ok(result.ok, "download works without the method option, defaults to GET"); + confirm("GET"); + + result = yield download({method: "PUT"}); + ok(!result.ok, "download rejected with PUT method"); + ok(/method: Invalid enumeration/.test(result.err), "descriptive error message"); + + result = yield download({method: "POST"}); + ok(result.ok, "download works with POST method"); + confirm("POST"); + + // Test body option values. + result = yield download({body: []}); + ok(!result.ok, "download rejected because of non-string body"); + ok(/body: Expected string/.test(result.err), "descriptive error message"); + + result = yield download({method: "POST", body: "of work"}); + ok(result.ok, "download works with POST method and body"); + confirm("POST", {"Content-Length": 7}, "of work"); + + // Test custom headers. + result = yield download({headers: [{name: "X-Custom"}]}); + ok(!result.ok, "download rejected because of missing header value"); + ok(/"value" is required/.test(result.err), "descriptive error message"); + + result = yield download({headers: [{name: "X-Custom", value: "13"}]}); + ok(result.ok, "download works with a custom header"); + confirm("GET", {"X-Custom": "13"}); + + // Test forbidden headers. + result = yield download({headers: [{name: "DNT", value: "1"}]}); + ok(!result.ok, "download rejected because of forbidden header name DNT"); + ok(/Forbidden request header/.test(result.err), "descriptive error message"); + + result = yield download({headers: [{name: "Proxy-Connection", value: "keep"}]}); + ok(!result.ok, "download rejected because of forbidden header name prefix Proxy-"); + ok(/Forbidden request header/.test(result.err), "descriptive error message"); + + result = yield download({headers: [{name: "Sec-ret", value: "13"}]}); + ok(!result.ok, "download rejected because of forbidden header name prefix Sec-"); + ok(/Forbidden request header/.test(result.err), "descriptive error message"); + + remove("post-log"); + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js new file mode 100644 index 000000000..d08aab666 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js @@ -0,0 +1,862 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +Cu.import("resource://gre/modules/Downloads.jsm"); + +const server = createHttpServer(); +server.registerDirectory("/data/", do_get_file("data")); + +const ROOT = `http://localhost:${server.identity.primaryPort}`; +const BASE = `${ROOT}/data`; +const TXT_FILE = "file_download.txt"; +const TXT_URL = BASE + "/" + TXT_FILE; + +// Keep these in sync with code in interruptible.sjs +const INT_PARTIAL_LEN = 15; +const INT_TOTAL_LEN = 31; + +const TEST_DATA = "This is 31 bytes of sample data"; +const TOTAL_LEN = TEST_DATA.length; +const PARTIAL_LEN = 15; + +// A handler to let us systematically test pausing/resuming/canceling +// of downloads. This target represents a small text file but a simple +// GET will stall after sending part of the data, to give the test code +// a chance to pause or do other operations on an in-progress download. +// A resumed download (ie, a GET with a Range: header) will allow the +// download to complete. +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain", false); + + if (request.hasHeader("Range")) { + let start, end; + let matches = request.getHeader("Range") + .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/); + if (matches != null) { + start = matches[1] ? parseInt(matches[1], 10) : 0; + end = matches[2] ? parseInt(matches[2], 10) : (TOTAL_LEN - 1); + } + + if (end == undefined || end >= TOTAL_LEN) { + response.setStatusLine(request.httpVersion, 416, "Requested Range Not Satisfiable"); + response.setHeader("Content-Range", `*/${TOTAL_LEN}`, false); + response.finish(); + return; + } + + response.setStatusLine(request.httpVersion, 206, "Partial Content"); + response.setHeader("Content-Range", `${start}-${end}/${TOTAL_LEN}`, false); + response.write(TEST_DATA.slice(start, end + 1)); + } else { + response.processAsync(); + response.setHeader("Content-Length", `${TOTAL_LEN}`, false); + response.write(TEST_DATA.slice(0, PARTIAL_LEN)); + } + + do_register_cleanup(() => { + try { + response.finish(); + } catch (e) { + // This will throw, but we don't care at this point. + } + }); +} + +server.registerPathHandler("/interruptible.html", handleRequest); + +let interruptibleCount = 0; +function getInterruptibleUrl() { + let n = interruptibleCount++; + return `${ROOT}/interruptible.html?count=${n}`; +} + +function backgroundScript() { + let events = new Set(); + let eventWaiter = null; + + browser.downloads.onCreated.addListener(data => { + events.add({type: "onCreated", data}); + if (eventWaiter) { + eventWaiter(); + } + }); + + browser.downloads.onChanged.addListener(data => { + events.add({type: "onChanged", data}); + if (eventWaiter) { + eventWaiter(); + } + }); + + browser.downloads.onErased.addListener(data => { + events.add({type: "onErased", data}); + if (eventWaiter) { + eventWaiter(); + } + }); + + // Returns a promise that will resolve when the given list of expected + // events have all been seen. By default, succeeds only if the exact list + // of expected events is seen in the given order. options.exact can be + // set to false to allow other events and options.inorder can be set to + // false to allow the events to arrive in any order. + function waitForEvents(expected, options = {}) { + function compare(a, b) { + if (typeof b == "object" && b != null) { + if (typeof a != "object") { + return false; + } + return Object.keys(b).every(fld => compare(a[fld], b[fld])); + } + return (a == b); + } + + const exact = ("exact" in options) ? options.exact : true; + const inorder = ("inorder" in options) ? options.inorder : true; + return new Promise((resolve, reject) => { + function check() { + function fail(msg) { + browser.test.fail(msg); + reject(new Error(msg)); + } + if (events.size < expected.length) { + return; + } + if (exact && expected.length < events.size) { + fail(`Got ${events.size} events but only expected ${expected.length}`); + return; + } + + let remaining = new Set(events); + if (inorder) { + for (let event of events) { + if (compare(event, expected[0])) { + expected.shift(); + remaining.delete(event); + } + } + } else { + expected = expected.filter(val => { + for (let remainingEvent of remaining) { + if (compare(remainingEvent, val)) { + remaining.delete(remainingEvent); + return false; + } + } + return true; + }); + } + + // Events that did occur have been removed from expected so if + // expected is empty, we're done. If we didn't see all the + // expected events and we're not looking for an exact match, + // then we just may not have seen the event yet, so return without + // failing and check() will be called again when a new event arrives. + if (expected.length == 0) { + events = remaining; + eventWaiter = null; + resolve(); + } else if (exact) { + fail(`Mismatched event: expecting ${JSON.stringify(expected[0])} but got ${JSON.stringify(Array.from(remaining)[0])}`); + } + } + eventWaiter = check; + check(); + }); + } + + browser.test.onMessage.addListener(async (msg, ...args) => { + let match = msg.match(/(\w+).request$/); + if (!match) { + return; + } + + let what = match[1]; + if (what == "waitForEvents") { + try { + await waitForEvents(...args); + browser.test.sendMessage("waitForEvents.done", {status: "success"}); + } catch (error) { + browser.test.sendMessage("waitForEvents.done", {status: "error", errmsg: error.message}); + } + } else if (what == "clearEvents") { + events = new Set(); + browser.test.sendMessage("clearEvents.done", {status: "success"}); + } else { + try { + let result = await browser.downloads[what](...args); + browser.test.sendMessage(`${what}.done`, {status: "success", result}); + } catch (error) { + browser.test.sendMessage(`${what}.done`, {status: "error", errmsg: error.message}); + } + } + }); + + browser.test.sendMessage("ready"); +} + +let downloadDir; +let extension; + +async function clearDownloads(callback) { + let list = await Downloads.getList(Downloads.ALL); + let downloads = await list.getAll(); + + await Promise.all(downloads.map(download => list.remove(download))); + + return downloads; +} + +function runInExtension(what, ...args) { + extension.sendMessage(`${what}.request`, ...args); + return extension.awaitMessage(`${what}.done`); +} + +// This is pretty simplistic, it looks for a progress update for a +// download of the given url in which the total bytes are exactly equal +// to the given value. Unless you know exactly how data will arrive from +// the server (eg see interruptible.sjs), it probably isn't very useful. +async function waitForProgress(url, bytes) { + let list = await Downloads.getList(Downloads.ALL); + + return new Promise(resolve => { + const view = { + onDownloadChanged(download) { + if (download.source.url == url && download.currentBytes == bytes) { + list.removeView(view); + resolve(); + } + }, + }; + list.addView(view); + }); +} + +add_task(function* setup() { + const nsIFile = Ci.nsIFile; + downloadDir = FileUtils.getDir("TmpD", ["downloads"]); + downloadDir.createUnique(nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + do_print(`downloadDir ${downloadDir.path}`); + + Services.prefs.setIntPref("browser.download.folderList", 2); + Services.prefs.setComplexValue("browser.download.dir", nsIFile, downloadDir); + + do_register_cleanup(() => { + Services.prefs.clearUserPref("browser.download.folderList"); + Services.prefs.clearUserPref("browser.download.dir"); + downloadDir.remove(true); + + return clearDownloads(); + }); + + yield clearDownloads().then(downloads => { + do_print(`removed ${downloads.length} pre-existing downloads from history`); + }); + + extension = ExtensionTestUtils.loadExtension({ + background: backgroundScript, + manifest: { + permissions: ["downloads"], + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); +}); + +add_task(function* test_events() { + let msg = yield runInExtension("download", {url: TXT_URL}); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + + msg = yield runInExtension("waitForEvents", [ + {type: "onCreated", data: {id, url: TXT_URL}}, + { + type: "onChanged", + data: { + id, + state: { + previous: "in_progress", + current: "complete", + }, + }, + }, + ]); + equal(msg.status, "success", "got onCreated and onChanged events"); +}); + +add_task(function* test_cancel() { + let url = getInterruptibleUrl(); + do_print(url); + let msg = yield runInExtension("download", {url}); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + + let progressPromise = waitForProgress(url, INT_PARTIAL_LEN); + + msg = yield runInExtension("waitForEvents", [ + {type: "onCreated", data: {id}}, + ]); + equal(msg.status, "success", "got created and changed events"); + + yield progressPromise; + do_print(`download reached ${INT_PARTIAL_LEN} bytes`); + + msg = yield runInExtension("cancel", id); + equal(msg.status, "success", "cancel() succeeded"); + + // This sequence of events is bogus (bug 1256243) + msg = yield runInExtension("waitForEvents", [ + { + type: "onChanged", + data: { + state: { + previous: "in_progress", + current: "interrupted", + }, + paused: { + previous: false, + current: true, + }, + }, + }, { + type: "onChanged", + data: { + id, + error: { + previous: null, + current: "USER_CANCELED", + }, + }, + }, { + type: "onChanged", + data: { + id, + paused: { + previous: true, + current: false, + }, + }, + }, + ]); + equal(msg.status, "success", "got onChanged events corresponding to cancel()"); + + msg = yield runInExtension("search", {error: "USER_CANCELED"}); + equal(msg.status, "success", "search() succeeded"); + equal(msg.result.length, 1, "search() found 1 download"); + equal(msg.result[0].id, id, "download.id is correct"); + equal(msg.result[0].state, "interrupted", "download.state is correct"); + equal(msg.result[0].paused, false, "download.paused is correct"); + equal(msg.result[0].canResume, false, "download.canResume is correct"); + equal(msg.result[0].error, "USER_CANCELED", "download.error is correct"); + equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct"); + equal(msg.result[0].exists, false, "download.exists is correct"); + + msg = yield runInExtension("pause", id); + equal(msg.status, "error", "cannot pause a canceled download"); + + msg = yield runInExtension("resume", id); + equal(msg.status, "error", "cannot resume a canceled download"); +}); + +add_task(function* test_pauseresume() { + let url = getInterruptibleUrl(); + let msg = yield runInExtension("download", {url}); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + + let progressPromise = waitForProgress(url, INT_PARTIAL_LEN); + + msg = yield runInExtension("waitForEvents", [ + {type: "onCreated", data: {id}}, + ]); + equal(msg.status, "success", "got created and changed events"); + + yield progressPromise; + do_print(`download reached ${INT_PARTIAL_LEN} bytes`); + + msg = yield runInExtension("pause", id); + equal(msg.status, "success", "pause() succeeded"); + + msg = yield runInExtension("waitForEvents", [ + { + type: "onChanged", + data: { + id, + state: { + previous: "in_progress", + current: "interrupted", + }, + paused: { + previous: false, + current: true, + }, + canResume: { + previous: false, + current: true, + }, + }, + }, { + type: "onChanged", + data: { + id, + error: { + previous: null, + current: "USER_CANCELED", + }, + }, + }]); + equal(msg.status, "success", "got onChanged event corresponding to pause"); + + msg = yield runInExtension("search", {paused: true}); + equal(msg.status, "success", "search() succeeded"); + equal(msg.result.length, 1, "search() found 1 download"); + equal(msg.result[0].id, id, "download.id is correct"); + equal(msg.result[0].state, "interrupted", "download.state is correct"); + equal(msg.result[0].paused, true, "download.paused is correct"); + equal(msg.result[0].canResume, true, "download.canResume is correct"); + equal(msg.result[0].error, "USER_CANCELED", "download.error is correct"); + equal(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct"); + equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct"); + equal(msg.result[0].exists, false, "download.exists is correct"); + + msg = yield runInExtension("search", {error: "USER_CANCELED"}); + equal(msg.status, "success", "search() succeeded"); + let found = msg.result.filter(item => item.id == id); + equal(found.length, 1, "search() by error found the paused download"); + + msg = yield runInExtension("pause", id); + equal(msg.status, "error", "cannot pause an already paused download"); + + msg = yield runInExtension("resume", id); + equal(msg.status, "success", "resume() succeeded"); + + msg = yield runInExtension("waitForEvents", [ + { + type: "onChanged", + data: { + id, + state: { + previous: "interrupted", + current: "in_progress", + }, + paused: { + previous: true, + current: false, + }, + canResume: { + previous: true, + current: false, + }, + error: { + previous: "USER_CANCELED", + current: null, + }, + }, + }, + { + type: "onChanged", + data: { + id, + state: { + previous: "in_progress", + current: "complete", + }, + }, + }, + ]); + equal(msg.status, "success", "got onChanged events for resume and complete"); + + msg = yield runInExtension("search", {id}); + equal(msg.status, "success", "search() succeeded"); + equal(msg.result.length, 1, "search() found 1 download"); + equal(msg.result[0].state, "complete", "download.state is correct"); + equal(msg.result[0].paused, false, "download.paused is correct"); + equal(msg.result[0].canResume, false, "download.canResume is correct"); + equal(msg.result[0].error, null, "download.error is correct"); + equal(msg.result[0].bytesReceived, INT_TOTAL_LEN, "download.bytesReceived is correct"); + equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct"); + equal(msg.result[0].exists, true, "download.exists is correct"); + + msg = yield runInExtension("pause", id); + equal(msg.status, "error", "cannot pause a completed download"); + + msg = yield runInExtension("resume", id); + equal(msg.status, "error", "cannot resume a completed download"); +}); + +add_task(function* test_pausecancel() { + let url = getInterruptibleUrl(); + let msg = yield runInExtension("download", {url}); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + + let progressPromise = waitForProgress(url, INT_PARTIAL_LEN); + + msg = yield runInExtension("waitForEvents", [ + {type: "onCreated", data: {id}}, + ]); + equal(msg.status, "success", "got created and changed events"); + + yield progressPromise; + do_print(`download reached ${INT_PARTIAL_LEN} bytes`); + + msg = yield runInExtension("pause", id); + equal(msg.status, "success", "pause() succeeded"); + + msg = yield runInExtension("waitForEvents", [ + { + type: "onChanged", + data: { + id, + state: { + previous: "in_progress", + current: "interrupted", + }, + paused: { + previous: false, + current: true, + }, + canResume: { + previous: false, + current: true, + }, + }, + }, { + type: "onChanged", + data: { + id, + error: { + previous: null, + current: "USER_CANCELED", + }, + }, + }]); + equal(msg.status, "success", "got onChanged event corresponding to pause"); + + msg = yield runInExtension("search", {paused: true}); + equal(msg.status, "success", "search() succeeded"); + equal(msg.result.length, 1, "search() found 1 download"); + equal(msg.result[0].id, id, "download.id is correct"); + equal(msg.result[0].state, "interrupted", "download.state is correct"); + equal(msg.result[0].paused, true, "download.paused is correct"); + equal(msg.result[0].canResume, true, "download.canResume is correct"); + equal(msg.result[0].error, "USER_CANCELED", "download.error is correct"); + equal(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct"); + equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct"); + equal(msg.result[0].exists, false, "download.exists is correct"); + + msg = yield runInExtension("search", {error: "USER_CANCELED"}); + equal(msg.status, "success", "search() succeeded"); + let found = msg.result.filter(item => item.id == id); + equal(found.length, 1, "search() by error found the paused download"); + + msg = yield runInExtension("cancel", id); + equal(msg.status, "success", "cancel() succeeded"); + + msg = yield runInExtension("waitForEvents", [ + { + type: "onChanged", + data: { + id, + paused: { + previous: true, + current: false, + }, + canResume: { + previous: true, + current: false, + }, + }, + }, + ]); + equal(msg.status, "success", "got onChanged event for cancel"); + + msg = yield runInExtension("search", {id}); + equal(msg.status, "success", "search() succeeded"); + equal(msg.result.length, 1, "search() found 1 download"); + equal(msg.result[0].state, "interrupted", "download.state is correct"); + equal(msg.result[0].paused, false, "download.paused is correct"); + equal(msg.result[0].canResume, false, "download.canResume is correct"); + equal(msg.result[0].error, "USER_CANCELED", "download.error is correct"); + equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct"); + equal(msg.result[0].exists, false, "download.exists is correct"); +}); + +add_task(function* test_pause_resume_cancel_badargs() { + let BAD_ID = 1000; + + let msg = yield runInExtension("pause", BAD_ID); + equal(msg.status, "error", "pause() failed with a bad download id"); + ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive"); + + msg = yield runInExtension("resume", BAD_ID); + equal(msg.status, "error", "resume() failed with a bad download id"); + ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive"); + + msg = yield runInExtension("cancel", BAD_ID); + equal(msg.status, "error", "cancel() failed with a bad download id"); + ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive"); +}); + +add_task(function* test_file_removal() { + let msg = yield runInExtension("download", {url: TXT_URL}); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + + msg = yield runInExtension("waitForEvents", [ + {type: "onCreated", data: {id, url: TXT_URL}}, + { + type: "onChanged", + data: { + id, + state: { + previous: "in_progress", + current: "complete", + }, + }, + }, + ]); + + equal(msg.status, "success", "got onCreated and onChanged events"); + + msg = yield runInExtension("removeFile", id); + equal(msg.status, "success", "removeFile() succeeded"); + + msg = yield runInExtension("removeFile", id); + equal(msg.status, "error", "removeFile() fails since the file was already removed."); + ok(/file doesn't exist/.test(msg.errmsg), "removeFile() failed on removed file."); + + msg = yield runInExtension("removeFile", 1000); + ok(/Invalid download id/.test(msg.errmsg), "removeFile() failed due to non-existent id"); +}); + +add_task(function* test_removal_of_incomplete_download() { + let url = getInterruptibleUrl(); + let msg = yield runInExtension("download", {url}); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + + let progressPromise = waitForProgress(url, INT_PARTIAL_LEN); + + msg = yield runInExtension("waitForEvents", [ + {type: "onCreated", data: {id}}, + ]); + equal(msg.status, "success", "got created and changed events"); + + yield progressPromise; + do_print(`download reached ${INT_PARTIAL_LEN} bytes`); + + msg = yield runInExtension("pause", id); + equal(msg.status, "success", "pause() succeeded"); + + msg = yield runInExtension("waitForEvents", [ + { + type: "onChanged", + data: { + id, + state: { + previous: "in_progress", + current: "interrupted", + }, + paused: { + previous: false, + current: true, + }, + canResume: { + previous: false, + current: true, + }, + }, + }, { + type: "onChanged", + data: { + id, + error: { + previous: null, + current: "USER_CANCELED", + }, + }, + }]); + equal(msg.status, "success", "got onChanged event corresponding to pause"); + + msg = yield runInExtension("removeFile", id); + equal(msg.status, "error", "removeFile() on paused download failed"); + + ok(/Cannot remove incomplete download/.test(msg.errmsg), "removeFile() failed due to download being incomplete"); + + msg = yield runInExtension("resume", id); + equal(msg.status, "success", "resume() succeeded"); + + msg = yield runInExtension("waitForEvents", [ + { + type: "onChanged", + data: { + id, + state: { + previous: "interrupted", + current: "in_progress", + }, + paused: { + previous: true, + current: false, + }, + canResume: { + previous: true, + current: false, + }, + error: { + previous: "USER_CANCELED", + current: null, + }, + }, + }, + { + type: "onChanged", + data: { + id, + state: { + previous: "in_progress", + current: "complete", + }, + }, + }, + ]); + equal(msg.status, "success", "got onChanged events for resume and complete"); + + msg = yield runInExtension("removeFile", id); + equal(msg.status, "success", "removeFile() succeeded following completion of resumed download."); +}); + +// Test erase(). We don't do elaborate testing of the query handling +// since it uses the exact same engine as search() which is tested +// more thoroughly in test_chrome_ext_downloads_search.html +add_task(function* test_erase() { + yield clearDownloads(); + + yield runInExtension("clearEvents"); + + function* download() { + let msg = yield runInExtension("download", {url: TXT_URL}); + equal(msg.status, "success", "download succeeded"); + let id = msg.result; + + msg = yield runInExtension("waitForEvents", [{ + type: "onChanged", data: {id, state: {current: "complete"}}, + }], {exact: false}); + equal(msg.status, "success", "download finished"); + + return id; + } + + let ids = {}; + ids.dl1 = yield download(); + ids.dl2 = yield download(); + ids.dl3 = yield download(); + + let msg = yield runInExtension("search", {}); + equal(msg.status, "success", "search succeded"); + equal(msg.result.length, 3, "search found 3 downloads"); + + msg = yield runInExtension("clearEvents"); + + msg = yield runInExtension("erase", {id: ids.dl1}); + equal(msg.status, "success", "erase by id succeeded"); + + msg = yield runInExtension("waitForEvents", [ + {type: "onErased", data: ids.dl1}, + ]); + equal(msg.status, "success", "received onErased event"); + + msg = yield runInExtension("search", {}); + equal(msg.status, "success", "search succeded"); + equal(msg.result.length, 2, "search found 2 downloads"); + + msg = yield runInExtension("erase", {}); + equal(msg.status, "success", "erase everything succeeded"); + + msg = yield runInExtension("waitForEvents", [ + {type: "onErased", data: ids.dl2}, + {type: "onErased", data: ids.dl3}, + ], {inorder: false}); + equal(msg.status, "success", "received 2 onErased events"); + + msg = yield runInExtension("search", {}); + equal(msg.status, "success", "search succeded"); + equal(msg.result.length, 0, "search found 0 downloads"); +}); + +function loadImage(img, data) { + return new Promise((resolve) => { + img.src = data; + img.onload = resolve; + }); +} + +add_task(function* test_getFileIcon() { + let webNav = Services.appShell.createWindowlessBrowser(false); + let docShell = webNav.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell); + + let system = Services.scriptSecurityManager.getSystemPrincipal(); + docShell.createAboutBlankContentViewer(system); + + let img = webNav.document.createElement("img"); + + let msg = yield runInExtension("download", {url: TXT_URL}); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + + msg = yield runInExtension("getFileIcon", id); + equal(msg.status, "success", "getFileIcon() succeeded"); + yield loadImage(img, msg.result); + equal(img.height, 32, "returns an icon with the right height"); + equal(img.width, 32, "returns an icon with the right width"); + + msg = yield runInExtension("waitForEvents", [ + {type: "onCreated", data: {id, url: TXT_URL}}, + {type: "onChanged"}, + ]); + equal(msg.status, "success", "got events"); + + msg = yield runInExtension("getFileIcon", id); + equal(msg.status, "success", "getFileIcon() succeeded"); + yield loadImage(img, msg.result); + equal(img.height, 32, "returns an icon with the right height after download"); + equal(img.width, 32, "returns an icon with the right width after download"); + + msg = yield runInExtension("getFileIcon", id + 100); + equal(msg.status, "error", "getFileIcon() failed"); + ok(msg.errmsg.includes("Invalid download id"), "download id is invalid"); + + msg = yield runInExtension("getFileIcon", id, {size: 127}); + equal(msg.status, "success", "getFileIcon() succeeded"); + yield loadImage(img, msg.result); + equal(img.height, 127, "returns an icon with the right custom height"); + equal(img.width, 127, "returns an icon with the right custom width"); + + msg = yield runInExtension("getFileIcon", id, {size: 1}); + equal(msg.status, "success", "getFileIcon() succeeded"); + yield loadImage(img, msg.result); + equal(img.height, 1, "returns an icon with the right custom height"); + equal(img.width, 1, "returns an icon with the right custom width"); + + msg = yield runInExtension("getFileIcon", id, {size: "foo"}); + equal(msg.status, "error", "getFileIcon() fails"); + ok(msg.errmsg.includes("Error processing size"), "size is not a number"); + + msg = yield runInExtension("getFileIcon", id, {size: 0}); + equal(msg.status, "error", "getFileIcon() fails"); + ok(msg.errmsg.includes("Error processing size"), "size is too small"); + + msg = yield runInExtension("getFileIcon", id, {size: 128}); + equal(msg.status, "error", "getFileIcon() fails"); + ok(msg.errmsg.includes("Error processing size"), "size is too big"); + + webNav.close(); +}); + +add_task(function* cleanup() { + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js new file mode 100644 index 000000000..4caa82456 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js @@ -0,0 +1,402 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +Cu.import("resource://gre/modules/Downloads.jsm"); + +const server = createHttpServer(); +server.registerDirectory("/data/", do_get_file("data")); + +const BASE = `http://localhost:${server.identity.primaryPort}/data`; +const TXT_FILE = "file_download.txt"; +const TXT_URL = BASE + "/" + TXT_FILE; +const TXT_LEN = 46; +const HTML_FILE = "file_download.html"; +const HTML_URL = BASE + "/" + HTML_FILE; +const HTML_LEN = 117; +const BIG_LEN = 1000; // something bigger both TXT_LEN and HTML_LEN + +function backgroundScript() { + let complete = new Map(); + + function waitForComplete(id) { + if (complete.has(id)) { + return complete.get(id).promise; + } + + let promise = new Promise(resolve => { + complete.set(id, {resolve}); + }); + complete.get(id).promise = promise; + return promise; + } + + browser.downloads.onChanged.addListener(change => { + if (change.state && change.state.current == "complete") { + // Make sure we have a promise. + waitForComplete(change.id); + complete.get(change.id).resolve(); + } + }); + + browser.test.onMessage.addListener(async (msg, ...args) => { + if (msg == "download.request") { + try { + let id = await browser.downloads.download(args[0]); + browser.test.sendMessage("download.done", {status: "success", id}); + } catch (error) { + browser.test.sendMessage("download.done", {status: "error", errmsg: error.message}); + } + } else if (msg == "search.request") { + try { + let downloads = await browser.downloads.search(args[0]); + browser.test.sendMessage("search.done", {status: "success", downloads}); + } catch (error) { + browser.test.sendMessage("search.done", {status: "error", errmsg: error.message}); + } + } else if (msg == "waitForComplete.request") { + await waitForComplete(args[0]); + browser.test.sendMessage("waitForComplete.done"); + } + }); + + browser.test.sendMessage("ready"); +} + +async function clearDownloads(callback) { + let list = await Downloads.getList(Downloads.ALL); + let downloads = await list.getAll(); + + await Promise.all(downloads.map(download => list.remove(download))); + + return downloads; +} + +add_task(function* test_search() { + const nsIFile = Ci.nsIFile; + let downloadDir = FileUtils.getDir("TmpD", ["downloads"]); + downloadDir.createUnique(nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + do_print(`downloadDir ${downloadDir.path}`); + + function downloadPath(filename) { + let path = downloadDir.clone(); + path.append(filename); + return path.path; + } + + Services.prefs.setIntPref("browser.download.folderList", 2); + Services.prefs.setComplexValue("browser.download.dir", nsIFile, downloadDir); + + do_register_cleanup(async () => { + Services.prefs.clearUserPref("browser.download.folderList"); + Services.prefs.clearUserPref("browser.download.dir"); + await cleanupDir(downloadDir); + await clearDownloads(); + }); + + yield clearDownloads().then(downloads => { + do_print(`removed ${downloads.length} pre-existing downloads from history`); + }); + + let extension = ExtensionTestUtils.loadExtension({ + background: backgroundScript, + manifest: { + permissions: ["downloads"], + }, + }); + + async function download(options) { + extension.sendMessage("download.request", options); + let result = await extension.awaitMessage("download.done"); + + if (result.status == "success") { + do_print(`wait for onChanged event to indicate ${result.id} is complete`); + extension.sendMessage("waitForComplete.request", result.id); + + await extension.awaitMessage("waitForComplete.done"); + } + + return result; + } + + function search(query) { + extension.sendMessage("search.request", query); + return extension.awaitMessage("search.done"); + } + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + // Do some downloads... + const time1 = new Date(); + + let downloadIds = {}; + let msg = yield download({url: TXT_URL}); + equal(msg.status, "success", "download() succeeded"); + downloadIds.txt1 = msg.id; + + const TXT_FILE2 = "NewFile.txt"; + msg = yield download({url: TXT_URL, filename: TXT_FILE2}); + equal(msg.status, "success", "download() succeeded"); + downloadIds.txt2 = msg.id; + + const time2 = new Date(); + + msg = yield download({url: HTML_URL}); + equal(msg.status, "success", "download() succeeded"); + downloadIds.html1 = msg.id; + + const HTML_FILE2 = "renamed.html"; + msg = yield download({url: HTML_URL, filename: HTML_FILE2}); + equal(msg.status, "success", "download() succeeded"); + downloadIds.html2 = msg.id; + + const time3 = new Date(); + + // Search for each individual download and check + // the corresponding DownloadItem. + function* checkDownloadItem(id, expect) { + let item = yield search({id}); + equal(item.status, "success", "search() succeeded"); + equal(item.downloads.length, 1, "search() found exactly 1 download"); + + Object.keys(expect).forEach(function(field) { + equal(item.downloads[0][field], expect[field], `DownloadItem.${field} is correct"`); + }); + } + yield checkDownloadItem(downloadIds.txt1, { + url: TXT_URL, + filename: downloadPath(TXT_FILE), + mime: "text/plain", + state: "complete", + bytesReceived: TXT_LEN, + totalBytes: TXT_LEN, + fileSize: TXT_LEN, + exists: true, + }); + + yield checkDownloadItem(downloadIds.txt2, { + url: TXT_URL, + filename: downloadPath(TXT_FILE2), + mime: "text/plain", + state: "complete", + bytesReceived: TXT_LEN, + totalBytes: TXT_LEN, + fileSize: TXT_LEN, + exists: true, + }); + + yield checkDownloadItem(downloadIds.html1, { + url: HTML_URL, + filename: downloadPath(HTML_FILE), + mime: "text/html", + state: "complete", + bytesReceived: HTML_LEN, + totalBytes: HTML_LEN, + fileSize: HTML_LEN, + exists: true, + }); + + yield checkDownloadItem(downloadIds.html2, { + url: HTML_URL, + filename: downloadPath(HTML_FILE2), + mime: "text/html", + state: "complete", + bytesReceived: HTML_LEN, + totalBytes: HTML_LEN, + fileSize: HTML_LEN, + exists: true, + }); + + function* checkSearch(query, expected, description, exact) { + let item = yield search(query); + equal(item.status, "success", "search() succeeded"); + equal(item.downloads.length, expected.length, `search() for ${description} found exactly ${expected.length} downloads`); + + let receivedIds = item.downloads.map(i => i.id); + if (exact) { + receivedIds.forEach((id, idx) => { + equal(id, downloadIds[expected[idx]], `search() for ${description} returned ${expected[idx]} in position ${idx}`); + }); + } else { + Object.keys(downloadIds).forEach(key => { + const id = downloadIds[key]; + const thisExpected = expected.includes(key); + equal(receivedIds.includes(id), thisExpected, + `search() for ${description} ${thisExpected ? "includes" : "does not include"} ${key}`); + }); + } + } + + // Check that search with an invalid id returns nothing. + // NB: for now ids are not persistent and we start numbering them at 1 + // so a sufficiently large number will be unused. + const INVALID_ID = 1000; + yield checkSearch({id: INVALID_ID}, [], "invalid id"); + + // Check that search on url works. + yield checkSearch({url: TXT_URL}, ["txt1", "txt2"], "url"); + + // Check that regexp on url works. + const HTML_REGEX = "[downlad]{8}\.html+$"; + yield checkSearch({urlRegex: HTML_REGEX}, ["html1", "html2"], "url regexp"); + + // Check that compatible url+regexp works + yield checkSearch({url: HTML_URL, urlRegex: HTML_REGEX}, ["html1", "html2"], "compatible url+urlRegex"); + + // Check that incompatible url+regexp works + yield checkSearch({url: TXT_URL, urlRegex: HTML_REGEX}, [], "incompatible url+urlRegex"); + + // Check that search on filename works. + yield checkSearch({filename: downloadPath(TXT_FILE)}, ["txt1"], "filename"); + + // Check that regexp on filename works. + yield checkSearch({filenameRegex: HTML_REGEX}, ["html1"], "filename regex"); + + // Check that compatible filename+regexp works + yield checkSearch({filename: downloadPath(HTML_FILE), filenameRegex: HTML_REGEX}, ["html1"], "compatible filename+filename regex"); + + // Check that incompatible filename+regexp works + yield checkSearch({filename: downloadPath(TXT_FILE), filenameRegex: HTML_REGEX}, [], "incompatible filename+filename regex"); + + // Check that simple positive search terms work. + yield checkSearch({query: ["file_download"]}, ["txt1", "txt2", "html1", "html2"], + "term file_download"); + yield checkSearch({query: ["NewFile"]}, ["txt2"], "term NewFile"); + + // Check that positive search terms work case-insensitive. + yield checkSearch({query: ["nEwfILe"]}, ["txt2"], "term nEwfiLe"); + + // Check that negative search terms work. + yield checkSearch({query: ["-txt"]}, ["html1", "html2"], "term -txt"); + + // Check that positive and negative search terms together work. + yield checkSearch({query: ["html", "-renamed"]}, ["html1"], "postive and negative terms"); + + function* checkSearchWithDate(query, expected, description) { + const fields = Object.keys(query); + if (fields.length != 1 || !(query[fields[0]] instanceof Date)) { + throw new Error("checkSearchWithDate expects exactly one Date field"); + } + const field = fields[0]; + const date = query[field]; + + let newquery = {}; + + // Check as a Date + newquery[field] = date; + yield checkSearch(newquery, expected, `${description} as Date`); + + // Check as numeric milliseconds + newquery[field] = date.valueOf(); + yield checkSearch(newquery, expected, `${description} as numeric ms`); + + // Check as stringified milliseconds + newquery[field] = date.valueOf().toString(); + yield checkSearch(newquery, expected, `${description} as string ms`); + + // Check as ISO string + newquery[field] = date.toISOString(); + yield checkSearch(newquery, expected, `${description} as iso string`); + } + + // Check startedBefore + yield checkSearchWithDate({startedBefore: time1}, [], "before time1"); + yield checkSearchWithDate({startedBefore: time2}, ["txt1", "txt2"], "before time2"); + yield checkSearchWithDate({startedBefore: time3}, ["txt1", "txt2", "html1", "html2"], "before time3"); + + // Check startedAfter + yield checkSearchWithDate({startedAfter: time1}, ["txt1", "txt2", "html1", "html2"], "after time1"); + yield checkSearchWithDate({startedAfter: time2}, ["html1", "html2"], "after time2"); + yield checkSearchWithDate({startedAfter: time3}, [], "after time3"); + + // Check simple search on totalBytes + yield checkSearch({totalBytes: TXT_LEN}, ["txt1", "txt2"], "totalBytes"); + yield checkSearch({totalBytes: HTML_LEN}, ["html1", "html2"], "totalBytes"); + + // Check simple test on totalBytes{Greater,Less} + // (NB: TXT_LEN < HTML_LEN < BIG_LEN) + yield checkSearch({totalBytesGreater: 0}, ["txt1", "txt2", "html1", "html2"], "totalBytesGreater than 0"); + yield checkSearch({totalBytesGreater: TXT_LEN}, ["html1", "html2"], `totalBytesGreater than ${TXT_LEN}`); + yield checkSearch({totalBytesGreater: HTML_LEN}, [], `totalBytesGreater than ${HTML_LEN}`); + yield checkSearch({totalBytesLess: TXT_LEN}, [], `totalBytesLess than ${TXT_LEN}`); + yield checkSearch({totalBytesLess: HTML_LEN}, ["txt1", "txt2"], `totalBytesLess than ${HTML_LEN}`); + yield checkSearch({totalBytesLess: BIG_LEN}, ["txt1", "txt2", "html1", "html2"], `totalBytesLess than ${BIG_LEN}`); + + // Check good combinations of totalBytes*. + yield checkSearch({totalBytes: HTML_LEN, totalBytesGreater: TXT_LEN}, ["html1", "html2"], "totalBytes and totalBytesGreater"); + yield checkSearch({totalBytes: TXT_LEN, totalBytesLess: HTML_LEN}, ["txt1", "txt2"], "totalBytes and totalBytesGreater"); + yield checkSearch({totalBytes: HTML_LEN, totalBytesLess: BIG_LEN, totalBytesGreater: 0}, ["html1", "html2"], "totalBytes and totalBytesLess and totalBytesGreater"); + + // Check bad combination of totalBytes*. + yield checkSearch({totalBytesLess: TXT_LEN, totalBytesGreater: HTML_LEN}, [], "bad totalBytesLess, totalBytesGreater combination"); + yield checkSearch({totalBytes: TXT_LEN, totalBytesGreater: HTML_LEN}, [], "bad totalBytes, totalBytesGreater combination"); + yield checkSearch({totalBytes: HTML_LEN, totalBytesLess: TXT_LEN}, [], "bad totalBytes, totalBytesLess combination"); + + // Check mime. + yield checkSearch({mime: "text/plain"}, ["txt1", "txt2"], "mime text/plain"); + yield checkSearch({mime: "text/html"}, ["html1", "html2"], "mime text/htmlplain"); + yield checkSearch({mime: "video/webm"}, [], "mime video/webm"); + + // Check fileSize. + yield checkSearch({fileSize: TXT_LEN}, ["txt1", "txt2"], "fileSize"); + yield checkSearch({fileSize: HTML_LEN}, ["html1", "html2"], "fileSize"); + + // Fields like bytesReceived, paused, state, exists are meaningful + // for downloads that are in progress but have not yet completed. + // todo: add tests for these when we have better support for in-progress + // downloads (e.g., after pause(), resume() and cancel() are implemented) + + // Check multiple query properties. + // We could make this testing arbitrarily complicated... + // We already tested combining fields with obvious interactions above + // (e.g., filename and filenameRegex or startTime and startedBefore/After) + // so now just throw as many fields as we can at a single search and + // make sure a simple case still works. + yield checkSearch({ + url: TXT_URL, + urlRegex: "download", + filename: downloadPath(TXT_FILE), + filenameRegex: "download", + query: ["download"], + startedAfter: time1.valueOf().toString(), + startedBefore: time2.valueOf().toString(), + totalBytes: TXT_LEN, + totalBytesGreater: 0, + totalBytesLess: BIG_LEN, + mime: "text/plain", + fileSize: TXT_LEN, + }, ["txt1"], "many properties"); + + // Check simple orderBy (forward and backward). + yield checkSearch({orderBy: ["startTime"]}, ["txt1", "txt2", "html1", "html2"], "orderBy startTime", true); + yield checkSearch({orderBy: ["-startTime"]}, ["html2", "html1", "txt2", "txt1"], "orderBy -startTime", true); + + // Check orderBy with multiple fields. + // NB: TXT_URL and HTML_URL differ only in extension and .html precedes .txt + yield checkSearch({orderBy: ["url", "-startTime"]}, ["html2", "html1", "txt2", "txt1"], "orderBy with multiple fields", true); + + // Check orderBy with limit. + yield checkSearch({orderBy: ["url"], limit: 1}, ["html1"], "orderBy with limit", true); + + // Check bad arguments. + function* checkBadSearch(query, pattern, description) { + let item = yield search(query); + equal(item.status, "error", "search() failed"); + ok(pattern.test(item.errmsg), `error message for ${description} was correct (${item.errmsg}).`); + } + + yield checkBadSearch("myquery", /Incorrect argument type/, "query is not an object"); + yield checkBadSearch({bogus: "boo"}, /Unexpected property/, "query contains an unknown field"); + yield checkBadSearch({query: "query string"}, /Expected array/, "query.query is a string"); + yield checkBadSearch({startedBefore: "i am not a time"}, /Type error/, "query.startedBefore is not a valid time"); + yield checkBadSearch({startedAfter: "i am not a time"}, /Type error/, "query.startedAfter is not a valid time"); + yield checkBadSearch({endedBefore: "i am not a time"}, /Type error/, "query.endedBefore is not a valid time"); + yield checkBadSearch({endedAfter: "i am not a time"}, /Type error/, "query.endedAfter is not a valid time"); + yield checkBadSearch({urlRegex: "["}, /Invalid urlRegex/, "query.urlRegexp is not a valid regular expression"); + yield checkBadSearch({filenameRegex: "["}, /Invalid filenameRegex/, "query.filenameRegexp is not a valid regular expression"); + yield checkBadSearch({orderBy: "startTime"}, /Expected array/, "query.orderBy is not an array"); + yield checkBadSearch({orderBy: ["bogus"]}, /Invalid orderBy field/, "query.orderBy references a non-existent field"); + + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_experiments.js b/toolkit/components/extensions/test/xpcshell/test_ext_experiments.js new file mode 100644 index 000000000..bc6bfcd68 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_experiments.js @@ -0,0 +1,175 @@ +"use strict"; + +/* globals browser */ + +XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", + "resource://gre/modules/AddonManager.jsm"); + +function promiseAddonStartup() { + const {Management} = Cu.import("resource://gre/modules/Extension.jsm"); + + return new Promise(resolve => { + let listener = (evt, extension) => { + Management.off("startup", listener); + resolve(extension); + }; + + Management.on("startup", listener); + }); +} + +add_task(function* setup() { + yield ExtensionTestUtils.startAddonManager(); +}); + +add_task(function* test_experiments_api() { + let apiAddonFile = Extension.generateZipFile({ + "install.rdf": `<?xml version="1.0" encoding="UTF-8"?> + <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest" + em:id="meh@experiments.addons.mozilla.org" + em:name="Meh Experiment" + em:type="256" + em:version="0.1" + em:description="Meh experiment" + em:creator="Mozilla"> + + <em:targetApplication> + <Description + em:id="xpcshell@tests.mozilla.org" + em:minVersion="48" + em:maxVersion="*"/> + </em:targetApplication> + </Description> + </RDF> + `, + + "api.js": String.raw` + Components.utils.import("resource://gre/modules/Services.jsm"); + + Services.obs.notifyObservers(null, "webext-api-loaded", ""); + + class API extends ExtensionAPI { + getAPI(context) { + return { + meh: { + hello(text) { + Services.obs.notifyObservers(null, "webext-api-hello", text); + } + } + } + } + } + `, + + "schema.json": [ + { + "namespace": "meh", + "description": "All full of meh.", + "permissions": ["experiments.meh"], + "functions": [ + { + "name": "hello", + "type": "function", + "description": "Hates you. This is all.", + "parameters": [ + {"type": "string", "name": "text"}, + ], + }, + ], + }, + ], + }); + + let addonFile = Extension.generateXPI({ + manifest: { + applications: {gecko: {id: "meh@web.extension"}}, + permissions: ["experiments.meh"], + }, + + background() { + // The test code below checks that hello() is called at the right + // time with the string "Here I am". Verify that the api schema is + // being correctly interpreted by calling hello() with bad arguments + // and only calling hello() with the magic string if the call with + // bad arguments throws. + try { + browser.meh.hello("I should not see this", "since two arguments are bad"); + } catch (err) { + browser.meh.hello("Here I am"); + } + }, + }); + + let boringAddonFile = Extension.generateXPI({ + manifest: { + applications: {gecko: {id: "boring@web.extension"}}, + }, + background() { + if (browser.meh) { + browser.meh.hello("Here I should not be"); + } + }, + }); + + do_register_cleanup(() => { + for (let file of [apiAddonFile, addonFile, boringAddonFile]) { + Services.obs.notifyObservers(file, "flush-cache-entry", null); + file.remove(false); + } + }); + + + let resolveHello; + let observer = (subject, topic, data) => { + if (topic == "webext-api-loaded") { + ok(!!resolveHello, "Should not see API loaded until dependent extension loads"); + } else if (topic == "webext-api-hello") { + resolveHello(data); + } + }; + + Services.obs.addObserver(observer, "webext-api-loaded", false); + Services.obs.addObserver(observer, "webext-api-hello", false); + do_register_cleanup(() => { + Services.obs.removeObserver(observer, "webext-api-loaded"); + Services.obs.removeObserver(observer, "webext-api-hello"); + }); + + + // Install API add-on. + let apiAddon = yield AddonManager.installTemporaryAddon(apiAddonFile); + + let {APIs} = Cu.import("resource://gre/modules/ExtensionManagement.jsm", {}); + ok(APIs.apis.has("meh"), "Should have meh API."); + + + // Install boring WebExtension add-on. + let boringAddon = yield AddonManager.installTemporaryAddon(boringAddonFile); + yield promiseAddonStartup(); + + + // Install interesting WebExtension add-on. + let promise = new Promise(resolve => { + resolveHello = resolve; + }); + + let addon = yield AddonManager.installTemporaryAddon(addonFile); + yield promiseAddonStartup(); + + let hello = yield promise; + equal(hello, "Here I am", "Should get hello from add-on"); + + // Cleanup. + apiAddon.uninstall(); + + boringAddon.userDisabled = true; + yield new Promise(do_execute_soon); + + equal(addon.appDisabled, true, "Add-on should be app-disabled after its dependency is removed."); + + addon.uninstall(); + boringAddon.uninstall(); +}); + diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_extension.js b/toolkit/components/extensions/test/xpcshell/test_ext_extension.js new file mode 100644 index 000000000..f18845f6a --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_extension.js @@ -0,0 +1,55 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_is_allowed_incognito_access() { + async function background() { + let allowed = await browser.extension.isAllowedIncognitoAccess(); + + browser.test.assertEq(true, allowed, "isAllowedIncognitoAccess is true"); + browser.test.notifyPass("isAllowedIncognitoAccess"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: {}, + }); + + yield extension.startup(); + yield extension.awaitFinish("isAllowedIncognitoAccess"); + yield extension.unload(); +}); + +add_task(function* test_in_incognito_context_false() { + function background() { + browser.test.assertEq(false, browser.extension.inIncognitoContext, "inIncognitoContext returned false"); + browser.test.notifyPass("inIncognitoContext"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: {}, + }); + + yield extension.startup(); + yield extension.awaitFinish("inIncognitoContext"); + yield extension.unload(); +}); + +add_task(function* test_is_allowed_file_scheme_access() { + async function background() { + let allowed = await browser.extension.isAllowedFileSchemeAccess(); + + browser.test.assertEq(false, allowed, "isAllowedFileSchemeAccess is false"); + browser.test.notifyPass("isAllowedFileSchemeAccess"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: {}, + }); + + yield extension.startup(); + yield extension.awaitFinish("isAllowedFileSchemeAccess"); + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_idle.js b/toolkit/components/extensions/test/xpcshell/test_ext_idle.js new file mode 100644 index 000000000..89bcac217 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_idle.js @@ -0,0 +1,202 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +Cu.import("resource://testing-common/MockRegistrar.jsm"); + +let idleService = { + _observers: new Set(), + _activity: { + addCalls: [], + removeCalls: [], + observerFires: [], + }, + _reset: function() { + this._observers.clear(); + this._activity.addCalls = []; + this._activity.removeCalls = []; + this._activity.observerFires = []; + }, + _fireObservers: function(state) { + for (let observer of this._observers.values()) { + observer.observe(observer, state, null); + this._activity.observerFires.push(state); + } + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIIdleService]), + idleTime: 19999, + addIdleObserver: function(observer, time) { + this._observers.add(observer); + this._activity.addCalls.push(time); + }, + removeIdleObserver: function(observer, time) { + this._observers.delete(observer); + this._activity.removeCalls.push(time); + }, +}; + +function checkActivity(expectedActivity) { + let {expectedAdd, expectedRemove, expectedFires} = expectedActivity; + let {addCalls, removeCalls, observerFires} = idleService._activity; + equal(expectedAdd.length, addCalls.length, "idleService.addIdleObserver was called the expected number of times"); + equal(expectedRemove.length, removeCalls.length, "idleService.removeIdleObserver was called the expected number of times"); + equal(expectedFires.length, observerFires.length, "idle observer was fired the expected number of times"); + deepEqual(addCalls, expectedAdd, "expected interval passed to idleService.addIdleObserver"); + deepEqual(removeCalls, expectedRemove, "expected interval passed to idleService.removeIdleObserver"); + deepEqual(observerFires, expectedFires, "expected topic passed to idle observer"); +} + +add_task(function* setup() { + let fakeIdleService = MockRegistrar.register("@mozilla.org/widget/idleservice;1", idleService); + do_register_cleanup(() => { + MockRegistrar.unregister(fakeIdleService); + }); +}); + +add_task(function* testQueryStateActive() { + function background() { + browser.idle.queryState(20).then(status => { + browser.test.assertEq("active", status, "Idle status is active"); + browser.test.notifyPass("idle"); + }, + err => { + browser.test.fail(`Error: ${err} :: ${err.stack}`); + browser.test.notifyFail("idle"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["idle"], + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("idle"); + yield extension.unload(); +}); + +add_task(function* testQueryStateIdle() { + function background() { + browser.idle.queryState(15).then(status => { + browser.test.assertEq("idle", status, "Idle status is idle"); + browser.test.notifyPass("idle"); + }, + err => { + browser.test.fail(`Error: ${err} :: ${err.stack}`); + browser.test.notifyFail("idle"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["idle"], + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("idle"); + yield extension.unload(); +}); + +add_task(function* testOnlySetDetectionInterval() { + function background() { + browser.idle.setDetectionInterval(99); + browser.test.sendMessage("detectionIntervalSet"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["idle"], + }, + }); + + idleService._reset(); + yield extension.startup(); + yield extension.awaitMessage("detectionIntervalSet"); + idleService._fireObservers("idle"); + checkActivity({expectedAdd: [], expectedRemove: [], expectedFires: []}); + yield extension.unload(); +}); + +add_task(function* testSetDetectionIntervalBeforeAddingListener() { + function background() { + browser.idle.setDetectionInterval(99); + browser.idle.onStateChanged.addListener(newState => { + browser.test.assertEq("idle", newState, "listener fired with the expected state"); + browser.test.sendMessage("listenerFired"); + }); + browser.test.sendMessage("listenerAdded"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["idle"], + }, + }); + + idleService._reset(); + yield extension.startup(); + yield extension.awaitMessage("listenerAdded"); + idleService._fireObservers("idle"); + yield extension.awaitMessage("listenerFired"); + checkActivity({expectedAdd: [99], expectedRemove: [], expectedFires: ["idle"]}); + yield extension.unload(); +}); + +add_task(function* testSetDetectionIntervalAfterAddingListener() { + function background() { + browser.idle.onStateChanged.addListener(newState => { + browser.test.assertEq("idle", newState, "listener fired with the expected state"); + browser.test.sendMessage("listenerFired"); + }); + browser.idle.setDetectionInterval(99); + browser.test.sendMessage("detectionIntervalSet"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["idle"], + }, + }); + + idleService._reset(); + yield extension.startup(); + yield extension.awaitMessage("detectionIntervalSet"); + idleService._fireObservers("idle"); + yield extension.awaitMessage("listenerFired"); + checkActivity({expectedAdd: [60, 99], expectedRemove: [60], expectedFires: ["idle"]}); + yield extension.unload(); +}); + +add_task(function* testOnlyAddingListener() { + function background() { + browser.idle.onStateChanged.addListener(newState => { + browser.test.assertEq("active", newState, "listener fired with the expected state"); + browser.test.sendMessage("listenerFired"); + }); + browser.test.sendMessage("listenerAdded"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["idle"], + }, + }); + + idleService._reset(); + yield extension.startup(); + yield extension.awaitMessage("listenerAdded"); + idleService._fireObservers("active"); + yield extension.awaitMessage("listenerFired"); + // check that "idle-daily" topic does not cause a listener to fire + idleService._fireObservers("idle-daily"); + checkActivity({expectedAdd: [60], expectedRemove: [], expectedFires: ["active", "idle-daily"]}); + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js b/toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js new file mode 100644 index 000000000..652f41315 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js @@ -0,0 +1,37 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_json_parser() { + const ID = "json@test.web.extension"; + + let xpi = Extension.generateXPI({ + files: { + "manifest.json": String.raw`{ + // This is a manifest. + "applications": {"gecko": {"id": "${ID}"}}, + "name": "This \" is // not a comment", + "version": "0.1\\" // , "description": "This is not a description" + }`, + }, + }); + + let expectedManifest = { + "applications": {"gecko": {"id": ID}}, + "name": "This \" is // not a comment", + "version": "0.1\\", + }; + + let fileURI = Services.io.newFileURI(xpi); + let uri = NetUtil.newURI(`jar:${fileURI.spec}!/`); + + let extension = new ExtensionData(uri); + + yield extension.readManifest(); + + Assert.deepEqual(extension.rawManifest, expectedManifest, + "Manifest with correctly-filtered comments"); + + Services.obs.notifyObservers(xpi, "flush-cache-entry", null); + xpi.remove(false); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_context.js b/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_context.js new file mode 100644 index 000000000..63d5361a1 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_context.js @@ -0,0 +1,168 @@ +"use strict"; + +/* globals browser */ + +Cu.import("resource://gre/modules/Extension.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +const {LegacyExtensionContext} = Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm"); + +/** + * This test case ensures that LegacyExtensionContext instances: + * - expose the expected API object and can join the messaging + * of a webextension given its addon id + * - the exposed API object can receive a port related to a `runtime.connect` + * Port created in the webextension's background page + * - the received Port instance can exchange messages with the background page + * - the received Port receive a disconnect event when the webextension is + * shutting down + */ +add_task(function* test_legacy_extension_context() { + function background() { + let bgURL = window.location.href; + + let extensionInfo = { + bgURL, + // Extract the assigned uuid from the background page url. + uuid: window.location.hostname, + }; + + browser.test.sendMessage("webextension-ready", extensionInfo); + + let port; + + browser.test.onMessage.addListener(async msg => { + if (msg == "do-send-message") { + let reply = await browser.runtime.sendMessage("webextension -> legacy_extension message"); + + browser.test.assertEq("legacy_extension -> webextension reply", reply, + "Got the expected message from the LegacyExtensionContext"); + browser.test.sendMessage("got-reply-message"); + } else if (msg == "do-connect") { + port = browser.runtime.connect(); + + port.onMessage.addListener(portMsg => { + browser.test.assertEq("legacy_extension -> webextension port message", portMsg, + "Got the expected message from the LegacyExtensionContext"); + port.postMessage("webextension -> legacy_extension port message"); + }); + } else if (msg == "do-disconnect") { + port.disconnect(); + } + }); + } + + let extensionData = { + background, + }; + + let extension = Extension.generate(extensionData); + + let waitForExtensionInfo = new Promise((resolve, reject) => { + extension.on("test-message", function testMessageListener(kind, msg, ...args) { + if (msg != "webextension-ready") { + reject(new Error(`Got an unexpected test-message: ${msg}`)); + } else { + extension.off("test-message", testMessageListener); + resolve(args[0]); + } + }); + }); + + // Connect to the target extension as an external context + // using the given custom sender info. + let legacyContext; + + extension.on("startup", function onStartup() { + extension.off("startup", onStartup); + legacyContext = new LegacyExtensionContext(extension); + extension.callOnClose({ + close: () => legacyContext.unload(), + }); + }); + + yield extension.startup(); + + let extensionInfo = yield waitForExtensionInfo; + + equal(legacyContext.envType, "legacy_extension", + "LegacyExtensionContext instance has the expected type"); + + ok(legacyContext.api, "Got the expected API object"); + ok(legacyContext.api.browser, "Got the expected browser property"); + + let waitMessage = new Promise(resolve => { + const {browser} = legacyContext.api; + browser.runtime.onMessage.addListener((singleMsg, msgSender) => { + resolve({singleMsg, msgSender}); + + // Send a reply to the sender. + return Promise.resolve("legacy_extension -> webextension reply"); + }); + }); + + extension.testMessage("do-send-message"); + + let {singleMsg, msgSender} = yield waitMessage; + equal(singleMsg, "webextension -> legacy_extension message", + "Got the expected message"); + ok(msgSender, "Got a message sender object"); + + equal(msgSender.id, extensionInfo.uuid, "The sender has the expected id property"); + equal(msgSender.url, extensionInfo.bgURL, "The sender has the expected url property"); + + // Wait confirmation that the reply has been received. + yield new Promise((resolve, reject) => { + extension.on("test-message", function testMessageListener(kind, msg, ...args) { + if (msg != "got-reply-message") { + reject(new Error(`Got an unexpected test-message: ${msg}`)); + } else { + extension.off("test-message", testMessageListener); + resolve(); + } + }); + }); + + let waitConnectPort = new Promise(resolve => { + let {browser} = legacyContext.api; + browser.runtime.onConnect.addListener(port => { + resolve(port); + }); + }); + + extension.testMessage("do-connect"); + + let port = yield waitConnectPort; + + ok(port, "Got the Port API object"); + ok(port.sender, "The port has a sender property"); + equal(port.sender.id, extensionInfo.uuid, + "The port sender has the expected id property"); + equal(port.sender.url, extensionInfo.bgURL, + "The port sender has the expected url property"); + + let waitPortMessage = new Promise(resolve => { + port.onMessage.addListener((msg) => { + resolve(msg); + }); + }); + + port.postMessage("legacy_extension -> webextension port message"); + + let msg = yield waitPortMessage; + + equal(msg, "webextension -> legacy_extension port message", + "LegacyExtensionContext received the expected message from the webextension"); + + let waitForDisconnect = new Promise(resolve => { + port.onDisconnect.addListener(resolve); + }); + + extension.testMessage("do-disconnect"); + + yield waitForDisconnect; + + do_print("Got the disconnect event on unload"); + + yield extension.shutdown(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_embedding.js b/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_embedding.js new file mode 100644 index 000000000..ea5d78524 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_embedding.js @@ -0,0 +1,188 @@ +"use strict"; + +/* globals browser */ + +Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm"); + +// Import EmbeddedExtensionManager to be able to check that the +// tacked instances are cleared after the embedded extension shutdown. +const { + EmbeddedExtensionManager, +} = Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm", {}); + +/** + * This test case ensures that the LegacyExtensionsUtils.EmbeddedExtension: + * - load the embedded webextension resources from a "/webextension/" dir + * inside the XPI. + * - EmbeddedExtension.prototype.api returns an API object which exposes + * a working `runtime.onConnect` event object (e.g. the API can receive a port + * when the embedded webextension is started and it can exchange messages + * with the background page). + * - EmbeddedExtension.prototype.startup/shutdown methods manage the embedded + * webextension lifecycle as expected. + */ +add_task(function* test_embedded_webextension_utils() { + function backgroundScript() { + let port = browser.runtime.connect(); + + port.onMessage.addListener((msg) => { + if (msg == "legacy_extension -> webextension") { + port.postMessage("webextension -> legacy_extension"); + port.disconnect(); + } + }); + } + + const id = "@test.embedded.web.extension"; + + // Extensions.generateXPI is used here (and in the other hybrid addons tests in this same + // test dir) to be able to generate an xpi with the directory layout that we expect from + // an hybrid legacy+webextension addon (where all the embedded webextension resources are + // loaded from a 'webextension/' directory). + let fakeHybridAddonFile = Extension.generateZipFile({ + "webextension/manifest.json": { + applications: {gecko: {id}}, + name: "embedded webextension name", + manifest_version: 2, + version: "1.0", + background: { + scripts: ["bg.js"], + }, + }, + "webextension/bg.js": `new ${backgroundScript}`, + }); + + // Remove the generated xpi file and flush the its jar cache + // on cleanup. + do_register_cleanup(() => { + Services.obs.notifyObservers(fakeHybridAddonFile, "flush-cache-entry", null); + fakeHybridAddonFile.remove(false); + }); + + let fileURI = Services.io.newFileURI(fakeHybridAddonFile); + let resourceURI = Services.io.newURI(`jar:${fileURI.spec}!/`, null, null); + + let embeddedExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({ + id, resourceURI, + }); + + ok(embeddedExtension, "Got the embeddedExtension object"); + + equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 1, + "Got the expected number of tracked embedded extension instances"); + + do_print("waiting embeddedExtension.startup"); + let embeddedExtensionAPI = yield embeddedExtension.startup(); + ok(embeddedExtensionAPI, "Got the embeddedExtensionAPI object"); + + let waitConnectPort = new Promise(resolve => { + let {browser} = embeddedExtensionAPI; + browser.runtime.onConnect.addListener(port => { + resolve(port); + }); + }); + + let port = yield waitConnectPort; + + ok(port, "Got the Port API object"); + + let waitPortMessage = new Promise(resolve => { + port.onMessage.addListener((msg) => { + resolve(msg); + }); + }); + + port.postMessage("legacy_extension -> webextension"); + + let msg = yield waitPortMessage; + + equal(msg, "webextension -> legacy_extension", + "LegacyExtensionContext received the expected message from the webextension"); + + let waitForDisconnect = new Promise(resolve => { + port.onDisconnect.addListener(resolve); + }); + + do_print("Wait for the disconnect port event"); + yield waitForDisconnect; + do_print("Got the disconnect port event"); + + yield embeddedExtension.shutdown(); + + equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0, + "EmbeddedExtension instances has been untracked from the EmbeddedExtensionManager"); +}); + +function* createManifestErrorTestCase(id, xpi, expectedError) { + // Remove the generated xpi file and flush the its jar cache + // on cleanup. + do_register_cleanup(() => { + Services.obs.notifyObservers(xpi, "flush-cache-entry", null); + xpi.remove(false); + }); + + let fileURI = Services.io.newFileURI(xpi); + let resourceURI = Services.io.newURI(`jar:${fileURI.spec}!/`, null, null); + + let embeddedExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({ + id, resourceURI, + }); + + yield Assert.rejects(embeddedExtension.startup(), expectedError, + "embedded extension startup rejected"); + + // Shutdown a "never-started" addon with an embedded webextension should not + // raise any exception, and if it does this test will fail. + yield embeddedExtension.shutdown(); +} + +add_task(function* test_startup_error_empty_manifest() { + const id = "empty-manifest@test.embedded.web.extension"; + const files = { + "webextension/manifest.json": ``, + }; + const expectedError = "(NS_BASE_STREAM_CLOSED)"; + + let fakeHybridAddonFile = Extension.generateZipFile(files); + + yield createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError); +}); + +add_task(function* test_startup_error_invalid_json_manifest() { + const id = "invalid-json-manifest@test.embedded.web.extension"; + const files = { + "webextension/manifest.json": `{ "name": }`, + }; + const expectedError = "JSON.parse:"; + + let fakeHybridAddonFile = Extension.generateZipFile(files); + + yield createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError); +}); + +add_task(function* test_startup_error_blocking_validation_errors() { + const id = "blocking-manifest-validation-error@test.embedded.web.extension"; + const files = { + "webextension/manifest.json": { + name: "embedded webextension name", + manifest_version: 2, + version: "1.0", + background: { + scripts: {}, + }, + }, + }; + + function expectedError(actual) { + if (actual.errors && actual.errors.length == 1 && + actual.errors[0].startsWith("Reading manifest:")) { + return true; + } + + return false; + } + + let fakeHybridAddonFile = Extension.generateZipFile(files); + + yield createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_localStorage.js b/toolkit/components/extensions/test/xpcshell/test_ext_localStorage.js new file mode 100644 index 000000000..0f0b41085 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_localStorage.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"; + +function backgroundScript() { + let hasRun = localStorage.getItem("has-run"); + let result; + if (!hasRun) { + localStorage.setItem("has-run", "yup"); + localStorage.setItem("test-item", "item1"); + result = "item1"; + } else { + let data = localStorage.getItem("test-item"); + if (data == "item1") { + localStorage.setItem("test-item", "item2"); + result = "item2"; + } else if (data == "item2") { + localStorage.removeItem("test-item"); + result = "deleted"; + } else if (!data) { + localStorage.clear(); + result = "cleared"; + } + } + browser.test.sendMessage("result", result); + browser.test.notifyPass("localStorage"); +} + +const ID = "test-webextension@mozilla.com"; +let extensionData = { + manifest: {applications: {gecko: {id: ID}}}, + background: backgroundScript, +}; + +add_task(function* test_localStorage() { + const RESULTS = ["item1", "item2", "deleted", "cleared", "item1"]; + + for (let expected of RESULTS) { + let extension = ExtensionTestUtils.loadExtension(extensionData); + + yield extension.startup(); + + let actual = yield extension.awaitMessage("result"); + + yield extension.awaitFinish("localStorage"); + yield extension.unload(); + + equal(actual, expected, "got expected localStorage data"); + } +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_management.js b/toolkit/components/extensions/test/xpcshell/test_ext_management.js new file mode 100644 index 000000000..b19554a57 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_management.js @@ -0,0 +1,20 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_management_schema() { + function background() { + browser.test.assertTrue(browser.management, "browser.management API exists"); + browser.test.notifyPass("management-schema"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["management"], + }, + background: `(${background})()`, + }); + yield extension.startup(); + yield extension.awaitFinish("management-schema"); + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js b/toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js new file mode 100644 index 000000000..7d80a9c23 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js @@ -0,0 +1,135 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +Cu.import("resource://gre/modules/AddonManager.jsm"); +Cu.import("resource://testing-common/AddonTestUtils.jsm"); +Cu.import("resource://testing-common/MockRegistrar.jsm"); + +const {promiseAddonByID} = AddonTestUtils; +const id = "uninstall_self_test@tests.mozilla.com"; + +const manifest = { + applications: { + gecko: { + id, + }, + }, + name: "test extension name", + version: "1.0", +}; + +const waitForUninstalled = () => new Promise(resolve => { + const listener = { + onUninstalled: (addon) => { + equal(addon.id, id, "The expected add-on has been uninstalled"); + AddonManager.getAddonByID(addon.id, checkedAddon => { + equal(checkedAddon, null, "Add-on no longer exists"); + AddonManager.removeAddonListener(listener); + resolve(); + }); + }, + }; + AddonManager.addAddonListener(listener); +}); + +let promptService = { + _response: null, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptService]), + confirmEx: function(...args) { + this._confirmExArgs = args; + return this._response; + }, +}; + +add_task(function* setup() { + let fakePromptService = MockRegistrar.register("@mozilla.org/embedcomp/prompt-service;1", promptService); + do_register_cleanup(() => { + MockRegistrar.unregister(fakePromptService); + }); + yield ExtensionTestUtils.startAddonManager(); +}); + +add_task(function* test_management_uninstall_no_prompt() { + function background() { + browser.test.onMessage.addListener(msg => { + browser.management.uninstallSelf(); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest, + background, + useAddonManager: "temporary", + }); + + yield extension.startup(); + let addon = yield promiseAddonByID(id); + notEqual(addon, null, "Add-on is installed"); + extension.sendMessage("uninstall"); + yield waitForUninstalled(); + yield extension.markUnloaded(); + Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null); +}); + +add_task(function* test_management_uninstall_prompt_uninstall() { + promptService._response = 0; + + function background() { + browser.test.onMessage.addListener(msg => { + browser.management.uninstallSelf({showConfirmDialog: true}); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest, + background, + useAddonManager: "temporary", + }); + + yield extension.startup(); + let addon = yield promiseAddonByID(id); + notEqual(addon, null, "Add-on is installed"); + extension.sendMessage("uninstall"); + yield waitForUninstalled(); + yield extension.markUnloaded(); + + // Test localization strings + equal(promptService._confirmExArgs[1], `Uninstall ${manifest.name}`); + equal(promptService._confirmExArgs[2], + `The extension “${manifest.name}†is requesting to be uninstalled. What would you like to do?`); + equal(promptService._confirmExArgs[4], "Uninstall"); + equal(promptService._confirmExArgs[5], "Keep Installed"); + Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null); +}); + +add_task(function* test_management_uninstall_prompt_keep() { + promptService._response = 1; + + function background() { + browser.test.onMessage.addListener(async msg => { + await browser.test.assertRejects( + browser.management.uninstallSelf({showConfirmDialog: true}), + "User cancelled uninstall of extension", + "Expected rejection when user declines uninstall"); + + browser.test.sendMessage("uninstall-rejected"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest, + background, + useAddonManager: "temporary", + }); + + yield extension.startup(); + let addon = yield promiseAddonByID(id); + notEqual(addon, null, "Add-on is installed"); + extension.sendMessage("uninstall"); + yield extension.awaitMessage("uninstall-rejected"); + addon = yield promiseAddonByID(id); + notEqual(addon, null, "Add-on remains installed"); + yield extension.unload(); + Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js new file mode 100644 index 000000000..2b0084980 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js @@ -0,0 +1,30 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + + +add_task(function* test_manifest_csp() { + let normalized = yield ExtensionTestUtils.normalizeManifest({ + "content_security_policy": "script-src 'self'; object-src 'none'", + }); + + equal(normalized.error, undefined, "Should not have an error"); + equal(normalized.errors.length, 0, "Should not have warnings"); + equal(normalized.value.content_security_policy, + "script-src 'self'; object-src 'none'", + "Should have the expected poilcy string"); + + + normalized = yield ExtensionTestUtils.normalizeManifest({ + "content_security_policy": "object-src 'none'", + }); + + equal(normalized.error, undefined, "Should not have an error"); + + Assert.deepEqual(normalized.errors, + ["Error processing content_security_policy: SyntaxError: Policy is missing a required \u2018script-src\u2019 directive"], + "Should have the expected warning"); + + equal(normalized.value.content_security_policy, null, + "Invalid policy string should be omitted"); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js new file mode 100644 index 000000000..94649692e --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js @@ -0,0 +1,27 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + + +add_task(function* test_manifest_incognito() { + let normalized = yield ExtensionTestUtils.normalizeManifest({ + "incognito": "spanning", + }); + + equal(normalized.error, undefined, "Should not have an error"); + equal(normalized.errors.length, 0, "Should not have warnings"); + equal(normalized.value.incognito, + "spanning", + "Should have the expected incognito string"); + + normalized = yield ExtensionTestUtils.normalizeManifest({ + "incognito": "split", + }); + + equal(normalized.error, undefined, "Should not have an error"); + Assert.deepEqual(normalized.errors, + ['Error processing incognito: Invalid enumeration value "split"'], + "Should have the expected warning"); + equal(normalized.value.incognito, null, + "Invalid incognito string should be omitted"); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_manifest_minimum_chrome_version.js b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_minimum_chrome_version.js new file mode 100644 index 000000000..fad5661bb --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_minimum_chrome_version.js @@ -0,0 +1,13 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + + +add_task(function* test_manifest_minimum_chrome_version() { + let normalized = yield ExtensionTestUtils.normalizeManifest({ + "minimum_chrome_version": "42", + }); + + equal(normalized.error, undefined, "Should not have an error"); + equal(normalized.errors.length, 0, "Should not have warnings"); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js new file mode 100644 index 000000000..5a6b628f5 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js @@ -0,0 +1,514 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* globals chrome */ + +const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes"; +const PREF_MAX_WRITE = "webextensions.native-messaging.max-output-message-bytes"; + +const ECHO_BODY = String.raw` + import struct + import sys + + while True: + rawlen = sys.stdin.read(4) + if len(rawlen) == 0: + sys.exit(0) + msglen = struct.unpack('@I', rawlen)[0] + msg = sys.stdin.read(msglen) + + sys.stdout.write(struct.pack('@I', msglen)) + sys.stdout.write(msg) +`; + +const INFO_BODY = String.raw` + import json + import os + import struct + import sys + + msg = json.dumps({"args": sys.argv, "cwd": os.getcwd()}) + sys.stdout.write(struct.pack('@I', len(msg))) + sys.stdout.write(msg) + sys.exit(0) +`; + +const STDERR_LINES = ["hello stderr", "this should be a separate line"]; +let STDERR_MSG = STDERR_LINES.join("\\n"); + +const STDERR_BODY = String.raw` + import sys + sys.stderr.write("${STDERR_MSG}") +`; + +const SCRIPTS = [ + { + name: "echo", + description: "a native app that echoes back messages it receives", + script: ECHO_BODY.replace(/^ {2}/gm, ""), + }, + { + name: "info", + description: "a native app that gives some info about how it was started", + script: INFO_BODY.replace(/^ {2}/gm, ""), + }, + { + name: "stderr", + description: "a native app that writes to stderr and then exits", + script: STDERR_BODY.replace(/^ {2}/gm, ""), + }, +]; + +add_task(function* setup() { + yield setupHosts(SCRIPTS); +}); + +// Test the basic operation of native messaging with a simple +// script that echoes back whatever message is sent to it. +add_task(function* test_happy_path() { + function background() { + let port = browser.runtime.connectNative("echo"); + port.onMessage.addListener(msg => { + browser.test.sendMessage("message", msg); + }); + browser.test.onMessage.addListener((what, payload) => { + if (what == "send") { + if (payload._json) { + let json = payload._json; + payload.toJSON = () => json; + delete payload._json; + } + port.postMessage(payload); + } + }); + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + const tests = [ + { + data: "this is a string", + what: "simple string", + }, + { + data: "Ðто юникода", + what: "unicode string", + }, + { + data: {test: "hello"}, + what: "simple object", + }, + { + data: { + what: "An object with a few properties", + number: 123, + bool: true, + nested: {what: "another object"}, + }, + what: "object with several properties", + }, + + { + data: { + ignoreme: true, + _json: {data: "i have a tojson method"}, + }, + expected: {data: "i have a tojson method"}, + what: "object with toJSON() method", + }, + ]; + for (let test of tests) { + extension.sendMessage("send", test.data); + let response = yield extension.awaitMessage("message"); + let expected = test.expected || test.data; + deepEqual(response, expected, `Echoed a message of type ${test.what}`); + } + + let procCount = yield getSubprocessCount(); + equal(procCount, 1, "subprocess is still running"); + let exitPromise = waitForSubprocessExit(); + yield extension.unload(); + yield exitPromise; +}); + +if (AppConstants.platform == "win") { + // "relative.echo" has a relative path in the host manifest. + add_task(function* test_relative_path() { + function background() { + let port = browser.runtime.connectNative("relative.echo"); + let MSG = "test relative echo path"; + port.onMessage.addListener(msg => { + browser.test.assertEq(MSG, msg, "Got expected message back"); + browser.test.sendMessage("done"); + }); + port.postMessage(MSG); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("done"); + + let procCount = yield getSubprocessCount(); + equal(procCount, 1, "subprocess is still running"); + let exitPromise = waitForSubprocessExit(); + yield extension.unload(); + yield exitPromise; + }); +} + +// Test sendNativeMessage() +add_task(function* test_sendNativeMessage() { + async function background() { + let MSG = {test: "hello world"}; + + // Check error handling + await browser.test.assertRejects( + browser.runtime.sendNativeMessage("nonexistent", MSG), + /Attempt to postMessage on disconnected port/, + "sendNativeMessage() to a nonexistent app failed"); + + // Check regular message exchange + let reply = await browser.runtime.sendNativeMessage("echo", MSG); + + let expected = JSON.stringify(MSG); + let received = JSON.stringify(reply); + browser.test.assertEq(expected, received, "Received echoed native message"); + + browser.test.sendMessage("finished"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("finished"); + + // With sendNativeMessage(), the subprocess should be disconnected + // after exchanging a single message. + yield waitForSubprocessExit(); + + yield extension.unload(); +}); + +// Test calling Port.disconnect() +add_task(function* test_disconnect() { + function background() { + let port = browser.runtime.connectNative("echo"); + port.onMessage.addListener((msg, msgPort) => { + browser.test.assertEq(port, msgPort, "onMessage handler should receive the port as the second argument"); + browser.test.sendMessage("message", msg); + }); + port.onDisconnect.addListener(msgPort => { + browser.test.fail("onDisconnect should not be called for disconnect()"); + }); + browser.test.onMessage.addListener((what, payload) => { + if (what == "send") { + if (payload._json) { + let json = payload._json; + payload.toJSON = () => json; + delete payload._json; + } + port.postMessage(payload); + } else if (what == "disconnect") { + try { + port.disconnect(); + browser.test.sendMessage("disconnect-result", {success: true}); + } catch (err) { + browser.test.sendMessage("disconnect-result", { + success: false, + errmsg: err.message, + }); + } + } + }); + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + extension.sendMessage("send", "test"); + let response = yield extension.awaitMessage("message"); + equal(response, "test", "Echoed a string"); + + let procCount = yield getSubprocessCount(); + equal(procCount, 1, "subprocess is running"); + + extension.sendMessage("disconnect"); + response = yield extension.awaitMessage("disconnect-result"); + equal(response.success, true, "disconnect succeeded"); + + do_print("waiting for subprocess to exit"); + yield waitForSubprocessExit(); + procCount = yield getSubprocessCount(); + equal(procCount, 0, "subprocess is no longer running"); + + extension.sendMessage("disconnect"); + response = yield extension.awaitMessage("disconnect-result"); + equal(response.success, true, "second call to disconnect silently ignored"); + + yield extension.unload(); +}); + +// Test the limit on message size for writing +add_task(function* test_write_limit() { + Services.prefs.setIntPref(PREF_MAX_WRITE, 10); + function clearPref() { + Services.prefs.clearUserPref(PREF_MAX_WRITE); + } + do_register_cleanup(clearPref); + + function background() { + const PAYLOAD = "0123456789A"; + let port = browser.runtime.connectNative("echo"); + try { + port.postMessage(PAYLOAD); + browser.test.sendMessage("result", null); + } catch (ex) { + browser.test.sendMessage("result", ex.message); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + + let errmsg = yield extension.awaitMessage("result"); + notEqual(errmsg, null, "native postMessage() failed for overly large message"); + + yield extension.unload(); + yield waitForSubprocessExit(); + + clearPref(); +}); + +// Test the limit on message size for reading +add_task(function* test_read_limit() { + Services.prefs.setIntPref(PREF_MAX_READ, 10); + function clearPref() { + Services.prefs.clearUserPref(PREF_MAX_READ); + } + do_register_cleanup(clearPref); + + function background() { + const PAYLOAD = "0123456789A"; + let port = browser.runtime.connectNative("echo"); + port.onDisconnect.addListener(msgPort => { + browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument"); + browser.test.assertEq("Native application tried to send a message of 13 bytes, which exceeds the limit of 10 bytes.", port.error && port.error.message); + browser.test.sendMessage("result", "disconnected"); + }); + port.onMessage.addListener(msg => { + browser.test.sendMessage("result", "message"); + }); + port.postMessage(PAYLOAD); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + + let result = yield extension.awaitMessage("result"); + equal(result, "disconnected", "native port disconnected on receiving large message"); + + yield extension.unload(); + yield waitForSubprocessExit(); + + clearPref(); +}); + +// Test that an extension without the nativeMessaging permission cannot +// use native messaging. +add_task(function* test_ext_permission() { + function background() { + browser.test.assertFalse("connectNative" in chrome.runtime, "chrome.runtime.connectNative does not exist without nativeMessaging permission"); + browser.test.assertFalse("connectNative" in browser.runtime, "browser.runtime.connectNative does not exist without nativeMessaging permission"); + browser.test.assertFalse("sendNativeMessage" in chrome.runtime, "chrome.runtime.sendNativeMessage does not exist without nativeMessaging permission"); + browser.test.assertFalse("sendNativeMessage" in browser.runtime, "browser.runtime.sendNativeMessage does not exist without nativeMessaging permission"); + browser.test.sendMessage("finished"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: {}, + }); + + yield extension.startup(); + yield extension.awaitMessage("finished"); + yield extension.unload(); +}); + +// Test that an extension that is not listed in allowed_extensions for +// a native application cannot use that application. +add_task(function* test_app_permission() { + function background() { + let port = browser.runtime.connectNative("echo"); + port.onDisconnect.addListener(msgPort => { + browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument"); + browser.test.assertEq("This extension does not have permission to use native application echo (or the application is not installed)", port.error && port.error.message); + browser.test.sendMessage("result", "disconnected"); + }); + port.onMessage.addListener(msg => { + browser.test.sendMessage("result", "message"); + }); + port.postMessage({test: "test"}); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["nativeMessaging"], + }, + }, "somethingelse@tests.mozilla.org"); + + yield extension.startup(); + + let result = yield extension.awaitMessage("result"); + equal(result, "disconnected", "connectNative() failed without native app permission"); + + yield extension.unload(); + + let procCount = yield getSubprocessCount(); + equal(procCount, 0, "No child process was started"); +}); + +// Test that the command-line arguments and working directory for the +// native application are as expected. +add_task(function* test_child_process() { + function background() { + let port = browser.runtime.connectNative("info"); + port.onMessage.addListener(msg => { + browser.test.sendMessage("result", msg); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + + let msg = yield extension.awaitMessage("result"); + equal(msg.args.length, 2, "Received one command line argument"); + equal(msg.args[1], getPath("info.json"), "Command line argument is the path to the native host manifest"); + equal(msg.cwd.replace(/^\/private\//, "/"), tmpDir.path, + "Working directory is the directory containing the native appliation"); + + let exitPromise = waitForSubprocessExit(); + yield extension.unload(); + yield exitPromise; +}); + +add_task(function* test_stderr() { + function background() { + let port = browser.runtime.connectNative("stderr"); + port.onDisconnect.addListener(msgPort => { + browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument"); + browser.test.assertEq(null, port.error, "Normal application exit is not an error"); + browser.test.sendMessage("finished"); + }); + } + + let {messages} = yield promiseConsoleOutput(function* () { + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("finished"); + yield extension.unload(); + + yield waitForSubprocessExit(); + }); + + let lines = STDERR_LINES.map(line => messages.findIndex(msg => msg.message.includes(line))); + notEqual(lines[0], -1, "Saw first line of stderr output on the console"); + notEqual(lines[1], -1, "Saw second line of stderr output on the console"); + notEqual(lines[0], lines[1], "Stderr output lines are separated in the console"); +}); + +// Test that calling connectNative() multiple times works +// (bug 1313980 was a previous regression in this area) +add_task(function* test_multiple_connects() { + async function background() { + function once() { + return new Promise(resolve => { + let MSG = "hello"; + let port = browser.runtime.connectNative("echo"); + + port.onMessage.addListener(msg => { + browser.test.assertEq(MSG, msg, "Got expected message back"); + port.disconnect(); + resolve(); + }); + port.postMessage(MSG); + }); + } + + await once(); + await once(); + browser.test.notifyPass("multiple-connect"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("multiple-connect"); + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_perf.js b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_perf.js new file mode 100644 index 000000000..693f67dde --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_perf.js @@ -0,0 +1,128 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +XPCOMUtils.defineLazyModuleGetter(this, "MockRegistry", + "resource://testing-common/MockRegistry.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); + +Cu.import("resource://gre/modules/Subprocess.jsm"); + +const MAX_ROUND_TRIP_TIME_MS = AppConstants.DEBUG || AppConstants.ASAN ? 36 : 18; +const MAX_RETRIES = 5; + + +const ECHO_BODY = String.raw` + import struct + import sys + + while True: + rawlen = sys.stdin.read(4) + if len(rawlen) == 0: + sys.exit(0) + + msglen = struct.unpack('@I', rawlen)[0] + msg = sys.stdin.read(msglen) + + sys.stdout.write(struct.pack('@I', msglen)) + sys.stdout.write(msg) +`; + +const SCRIPTS = [ + { + name: "echo", + description: "A native app that echoes back messages it receives", + script: ECHO_BODY.replace(/^ {2}/gm, ""), + }, +]; + +add_task(function* setup() { + yield setupHosts(SCRIPTS); +}); + +add_task(function* test_round_trip_perf() { + let extension = ExtensionTestUtils.loadExtension({ + background() { + browser.test.onMessage.addListener(msg => { + if (msg != "run-tests") { + return; + } + + let port = browser.runtime.connectNative("echo"); + + function next() { + port.postMessage({ + "Lorem": { + "ipsum": { + "dolor": [ + "sit amet", + "consectetur adipiscing elit", + "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ], + "Ut enim": [ + "ad minim veniam", + "quis nostrud exercitation ullamco", + "laboris nisi ut aliquip ex ea commodo consequat.", + ], + "Duis": [ + "aute irure dolor in reprehenderit in", + "voluptate velit esse cillum dolore eu", + "fugiat nulla pariatur.", + ], + "Excepteur": [ + "sint occaecat cupidatat non proident", + "sunt in culpa qui officia deserunt", + "mollit anim id est laborum.", + ], + }, + }, + }); + } + + const COUNT = 1000; + let now; + function finish() { + let roundTripTime = (Date.now() - now) / COUNT; + + port.disconnect(); + browser.test.sendMessage("result", roundTripTime); + } + + let count = 0; + port.onMessage.addListener(() => { + if (count == 0) { + // Skip the first round, since it includes the time it takes + // the app to start up. + now = Date.now(); + } + + if (count++ <= COUNT) { + next(); + } else { + finish(); + } + }); + + next(); + }); + }, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + + let roundTripTime = Infinity; + for (let i = 0; i < MAX_RETRIES && roundTripTime > MAX_ROUND_TRIP_TIME_MS; i++) { + extension.sendMessage("run-tests"); + roundTripTime = yield extension.awaitMessage("result"); + } + + yield extension.unload(); + + ok(roundTripTime <= MAX_ROUND_TRIP_TIME_MS, + `Expected round trip time (${roundTripTime}ms) to be less than ${MAX_ROUND_TRIP_TIME_MS}ms`); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_unresponsive.js b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_unresponsive.js new file mode 100644 index 000000000..a75a1d49d --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_unresponsive.js @@ -0,0 +1,82 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const WONTDIE_BODY = String.raw` + import signal + import struct + import sys + import time + + signal.signal(signal.SIGTERM, signal.SIG_IGN) + + def spin(): + while True: + try: + signal.pause() + except AttributeError: + time.sleep(5) + + while True: + rawlen = sys.stdin.read(4) + if len(rawlen) == 0: + spin() + + msglen = struct.unpack('@I', rawlen)[0] + msg = sys.stdin.read(msglen) + + sys.stdout.write(struct.pack('@I', msglen)) + sys.stdout.write(msg) +`; + +const SCRIPTS = [ + { + name: "wontdie", + description: "a native app that does not exit when stdin closes or on SIGTERM", + script: WONTDIE_BODY.replace(/^ {2}/gm, ""), + }, +]; + +add_task(function* setup() { + yield setupHosts(SCRIPTS); +}); + + +// Test that an unresponsive native application still gets killed eventually +add_task(function* test_unresponsive_native_app() { + // XXX expose GRACEFUL_SHUTDOWN_TIME as a pref and reduce it + // just for this test? + + function background() { + let port = browser.runtime.connectNative("wontdie"); + + const MSG = "echo me"; + // bounce a message to make sure the process actually starts + port.onMessage.addListener(msg => { + browser.test.assertEq(msg, MSG, "Received echoed message"); + browser.test.sendMessage("ready"); + }); + port.postMessage(MSG); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + let procCount = yield getSubprocessCount(); + equal(procCount, 1, "subprocess is running"); + + let exitPromise = waitForSubprocessExit(); + yield extension.unload(); + yield exitPromise; + + procCount = yield getSubprocessCount(); + equal(procCount, 0, "subprocess was succesfully killed"); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_onmessage_removelistener.js b/toolkit/components/extensions/test/xpcshell/test_ext_onmessage_removelistener.js new file mode 100644 index 000000000..6f8b553fc --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_onmessage_removelistener.js @@ -0,0 +1,30 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function backgroundScript() { + function listener() { + browser.test.notifyFail("listener should not be invoked"); + } + + browser.runtime.onMessage.addListener(listener); + browser.runtime.onMessage.removeListener(listener); + browser.runtime.sendMessage("hello"); + + // Make sure that, if we somehow fail to remove the listener, then we'll run + // the listener before the test is marked as passing. + setTimeout(function() { + browser.test.notifyPass("onmessage_removelistener"); + }, 0); +} + +let extensionData = { + background: backgroundScript, +}; + +add_task(function* test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + yield extension.awaitFinish("onmessage_removelistener"); + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_connect_no_receiver.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_connect_no_receiver.js new file mode 100644 index 000000000..2a1342cde --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_connect_no_receiver.js @@ -0,0 +1,23 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_connect_without_listener() { + function background() { + let port = browser.runtime.connect(); + port.onDisconnect.addListener(() => { + browser.test.assertEq("Could not establish connection. Receiving end does not exist.", port.error && port.error.message); + browser.test.notifyPass("port.onDisconnect was called"); + }); + } + let extensionData = { + background, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + yield extension.awaitFinish("port.onDisconnect was called"); + + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getBrowserInfo.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getBrowserInfo.js new file mode 100644 index 000000000..a280206fa --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getBrowserInfo.js @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +add_task(function* setup() { + ExtensionTestUtils.mockAppInfo(); +}); + +add_task(function* test_getBrowserInfo() { + async function background() { + let info = await browser.runtime.getBrowserInfo(); + + browser.test.assertEq(info.name, "XPCShell", "name is valid"); + browser.test.assertEq(info.vendor, "Mozilla", "vendor is Mozilla"); + browser.test.assertEq(info.version, "48", "version is correct"); + browser.test.assertEq(info.buildID, "20160315", "buildID is correct"); + + browser.test.notifyPass("runtime.getBrowserInfo"); + } + + const extension = ExtensionTestUtils.loadExtension({background}); + yield extension.startup(); + yield extension.awaitFinish("runtime.getBrowserInfo"); + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getPlatformInfo.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getPlatformInfo.js new file mode 100644 index 000000000..29bad0c10 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getPlatformInfo.js @@ -0,0 +1,25 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function backgroundScript() { + browser.runtime.getPlatformInfo(info => { + let validOSs = ["mac", "win", "android", "cros", "linux", "openbsd"]; + let validArchs = ["arm", "x86-32", "x86-64"]; + + browser.test.assertTrue(validOSs.indexOf(info.os) != -1, "OS is valid"); + browser.test.assertTrue(validArchs.indexOf(info.arch) != -1, "Architecture is valid"); + browser.test.notifyPass("runtime.getPlatformInfo"); + }); +} + +let extensionData = { + background: backgroundScript, +}; + +add_task(function* test_contentscript() { + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + yield extension.awaitFinish("runtime.getPlatformInfo"); + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js new file mode 100644 index 000000000..fa6461412 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js @@ -0,0 +1,337 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +XPCOMUtils.defineLazyGetter(this, "Management", () => { + const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {}); + return Management; +}); + +const { + createAppInfo, + createTempWebExtensionFile, + promiseAddonByID, + promiseAddonEvent, + promiseCompleteAllInstalls, + promiseFindAddonUpdates, + promiseRestartManager, + promiseShutdownManager, + promiseStartupManager, +} = AddonTestUtils; + +AddonTestUtils.init(this); + +// Allow for unsigned addons. +AddonTestUtils.overrideCertDB(); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +function awaitEvent(eventName) { + return new Promise(resolve => { + let listener = (_eventName, ...args) => { + if (_eventName === eventName) { + Management.off(eventName, listener); + resolve(...args); + } + }; + + Management.on(eventName, listener); + }); +} + +function background() { + let onInstalledDetails = null; + let onStartupFired = false; + + browser.runtime.onInstalled.addListener(details => { + onInstalledDetails = details; + }); + + browser.runtime.onStartup.addListener(() => { + onStartupFired = true; + }); + + browser.test.onMessage.addListener(message => { + if (message === "get-on-installed-details") { + onInstalledDetails = onInstalledDetails || {fired: false}; + browser.test.sendMessage("on-installed-details", onInstalledDetails); + } else if (message === "did-on-startup-fire") { + browser.test.sendMessage("on-startup-fired", onStartupFired); + } else if (message === "reload-extension") { + browser.runtime.reload(); + } + }); + + browser.runtime.onUpdateAvailable.addListener(details => { + browser.test.sendMessage("reloading"); + browser.runtime.reload(); + }); +} + +function* expectEvents(extension, {onStartupFired, onInstalledFired, onInstalledReason}) { + extension.sendMessage("get-on-installed-details"); + let details = yield extension.awaitMessage("on-installed-details"); + if (onInstalledFired) { + equal(details.reason, onInstalledReason, "runtime.onInstalled fired with the correct reason"); + } else { + equal(details.fired, onInstalledFired, "runtime.onInstalled should not have fired"); + } + + extension.sendMessage("did-on-startup-fire"); + let fired = yield extension.awaitMessage("on-startup-fired"); + equal(fired, onStartupFired, `Expected runtime.onStartup to ${onStartupFired ? "" : "not "} fire`); +} + +add_task(function* test_should_fire_on_addon_update() { + const EXTENSION_ID = "test_runtime_on_installed_addon_update@tests.mozilla.org"; + + const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity"; + + // The test extension uses an insecure update url. + Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); + + const testServer = createHttpServer(); + const port = testServer.identity.primaryPort; + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + "version": "1.0", + "applications": { + "gecko": { + "id": EXTENSION_ID, + "update_url": `http://localhost:${port}/test_update.json`, + }, + }, + }, + background, + }); + + testServer.registerPathHandler("/test_update.json", (request, response) => { + response.write(`{ + "addons": { + "${EXTENSION_ID}": { + "updates": [ + { + "version": "2.0", + "update_link": "http://localhost:${port}/addons/test_runtime_on_installed-2.0.xpi" + } + ] + } + } + }`); + }); + + let webExtensionFile = createTempWebExtensionFile({ + manifest: { + version: "2.0", + applications: { + gecko: { + id: EXTENSION_ID, + }, + }, + }, + background, + }); + + testServer.registerFile("/addons/test_runtime_on_installed-2.0.xpi", webExtensionFile); + + yield promiseStartupManager(); + + yield extension.startup(); + + yield expectEvents(extension, { + onStartupFired: false, + onInstalledFired: true, + onInstalledReason: "install", + }); + + let addon = yield promiseAddonByID(EXTENSION_ID); + equal(addon.version, "1.0", "The installed addon has the correct version"); + + let update = yield promiseFindAddonUpdates(addon); + let install = update.updateAvailable; + + let promiseInstalled = promiseAddonEvent("onInstalled"); + yield promiseCompleteAllInstalls([install]); + + yield extension.awaitMessage("reloading"); + + let startupPromise = awaitEvent("ready"); + + let [updated_addon] = yield promiseInstalled; + equal(updated_addon.version, "2.0", "The updated addon has the correct version"); + + extension.extension = yield startupPromise; + extension.attachListeners(); + + yield expectEvents(extension, { + onStartupFired: false, + onInstalledFired: true, + onInstalledReason: "update", + }); + + yield extension.unload(); + + yield updated_addon.uninstall(); + yield promiseShutdownManager(); +}); + +add_task(function* test_should_fire_on_browser_update() { + const EXTENSION_ID = "test_runtime_on_installed_browser_update@tests.mozilla.org"; + + yield promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + "version": "1.0", + "applications": { + "gecko": { + "id": EXTENSION_ID, + }, + }, + }, + background, + }); + + yield extension.startup(); + + yield expectEvents(extension, { + onStartupFired: false, + onInstalledFired: true, + onInstalledReason: "install", + }); + + let startupPromise = awaitEvent("ready"); + yield promiseRestartManager("1"); + extension.extension = yield startupPromise; + extension.attachListeners(); + + yield expectEvents(extension, { + onStartupFired: true, + onInstalledFired: false, + }); + + // Update the browser. + startupPromise = awaitEvent("ready"); + yield promiseRestartManager("2"); + extension.extension = yield startupPromise; + extension.attachListeners(); + + yield expectEvents(extension, { + onStartupFired: true, + onInstalledFired: true, + onInstalledReason: "browser_update", + }); + + // Restart the browser. + startupPromise = awaitEvent("ready"); + yield promiseRestartManager("2"); + extension.extension = yield startupPromise; + extension.attachListeners(); + + yield expectEvents(extension, { + onStartupFired: true, + onInstalledFired: false, + }); + + // Update the browser again. + startupPromise = awaitEvent("ready"); + yield promiseRestartManager("3"); + extension.extension = yield startupPromise; + extension.attachListeners(); + + yield expectEvents(extension, { + onStartupFired: true, + onInstalledFired: true, + onInstalledReason: "browser_update", + }); + + yield extension.unload(); + + yield promiseShutdownManager(); +}); + +add_task(function* test_should_not_fire_on_reload() { + const EXTENSION_ID = "test_runtime_on_installed_reload@tests.mozilla.org"; + + yield promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + "version": "1.0", + "applications": { + "gecko": { + "id": EXTENSION_ID, + }, + }, + }, + background, + }); + + yield extension.startup(); + + yield expectEvents(extension, { + onStartupFired: false, + onInstalledFired: true, + onInstalledReason: "install", + }); + + let startupPromise = awaitEvent("ready"); + extension.sendMessage("reload-extension"); + extension.extension = yield startupPromise; + extension.attachListeners(); + + yield expectEvents(extension, { + onStartupFired: false, + onInstalledFired: false, + }); + + yield extension.unload(); + yield promiseShutdownManager(); +}); + +add_task(function* test_should_not_fire_on_restart() { + const EXTENSION_ID = "test_runtime_on_installed_restart@tests.mozilla.org"; + + yield promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + "version": "1.0", + "applications": { + "gecko": { + "id": EXTENSION_ID, + }, + }, + }, + background, + }); + + yield extension.startup(); + + yield expectEvents(extension, { + onStartupFired: false, + onInstalledFired: true, + onInstalledReason: "install", + }); + + let addon = yield promiseAddonByID(EXTENSION_ID); + addon.userDisabled = true; + + let startupPromise = awaitEvent("ready"); + addon.userDisabled = false; + extension.extension = yield startupPromise; + extension.attachListeners(); + + yield expectEvents(extension, { + onStartupFired: false, + onInstalledFired: false, + }); + + yield extension.markUnloaded(); + yield promiseShutdownManager(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage.js new file mode 100644 index 000000000..fec8e13dd --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage.js @@ -0,0 +1,79 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* tabsSendMessageReply() { + function background() { + browser.runtime.onMessage.addListener((msg, sender, respond) => { + if (msg == "respond-now") { + respond(msg); + } else if (msg == "respond-soon") { + setTimeout(() => { respond(msg); }, 0); + return true; + } else if (msg == "respond-promise") { + return Promise.resolve(msg); + } else if (msg == "respond-never") { + return; + } else if (msg == "respond-error") { + return Promise.reject(new Error(msg)); + } else if (msg == "throw-error") { + throw new Error(msg); + } + }); + + browser.runtime.onMessage.addListener((msg, sender, respond) => { + if (msg == "respond-now") { + respond("hello"); + } else if (msg == "respond-now-2") { + respond(msg); + } + }); + + let childFrame = document.createElement("iframe"); + childFrame.src = "extensionpage.html"; + document.body.appendChild(childFrame); + } + + function senderScript() { + Promise.all([ + browser.runtime.sendMessage("respond-now"), + browser.runtime.sendMessage("respond-now-2"), + new Promise(resolve => browser.runtime.sendMessage("respond-soon", resolve)), + browser.runtime.sendMessage("respond-promise"), + browser.runtime.sendMessage("respond-never"), + new Promise(resolve => { + browser.runtime.sendMessage("respond-never", response => { resolve(response); }); + }), + + browser.runtime.sendMessage("respond-error").catch(error => Promise.resolve({error})), + browser.runtime.sendMessage("throw-error").catch(error => Promise.resolve({error})), + ]).then(([respondNow, respondNow2, respondSoon, respondPromise, respondNever, respondNever2, respondError, throwError]) => { + browser.test.assertEq("respond-now", respondNow, "Got the expected immediate response"); + browser.test.assertEq("respond-now-2", respondNow2, "Got the expected immediate response from the second listener"); + browser.test.assertEq("respond-soon", respondSoon, "Got the expected delayed response"); + browser.test.assertEq("respond-promise", respondPromise, "Got the expected promise response"); + browser.test.assertEq(undefined, respondNever, "Got the expected no-response resolution"); + browser.test.assertEq(undefined, respondNever2, "Got the expected no-response resolution"); + + browser.test.assertEq("respond-error", respondError.error.message, "Got the expected error response"); + browser.test.assertEq("throw-error", throwError.error.message, "Got the expected thrown error response"); + + browser.test.notifyPass("sendMessage"); + }).catch(e => { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("sendMessage"); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + files: { + "senderScript.js": senderScript, + "extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="senderScript.js"></script>`, + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("sendMessage"); + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_errors.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_errors.js new file mode 100644 index 000000000..f1a8d5a36 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_errors.js @@ -0,0 +1,59 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_sendMessage_error() { + async function background() { + let circ = {}; + circ.circ = circ; + let testCases = [ + // [arguments, expected error string], + [[], "runtime.sendMessage's message argument is missing"], + [[null, null, null, null], "runtime.sendMessage's last argument is not a function"], + [[null, null, 1], "runtime.sendMessage's options argument is invalid"], + [[1, null, null], "runtime.sendMessage's extensionId argument is invalid"], + [[null, null, null, null, null], "runtime.sendMessage received too many arguments"], + + // Even when the parameters are accepted, we still expect an error + // because there is no onMessage listener. + [[null, null, null], "Could not establish connection. Receiving end does not exist."], + + // Structural cloning doesn't work with DOM but we fall back + // JSON serialization, so we don't expect another error. + [[null, location, null], "Could not establish connection. Receiving end does not exist."], + + // Structured cloning supports cyclic self-references. + [[null, [circ, location], null], "cyclic object value"], + // JSON serialization does not support cyclic references. + [[null, circ, null], "Could not establish connection. Receiving end does not exist."], + // (the last two tests shows whether sendMessage is implemented as structured cloning). + ]; + + // Repeat all tests with the undefined value instead of null. + for (let [args, expectedError] of testCases.slice()) { + args = args.map(arg => arg === null ? undefined : arg); + testCases.push([args, expectedError]); + } + + for (let [args, expectedError] of testCases) { + let description = `runtime.sendMessage(${args.map(String).join(", ")})`; + + await browser.test.assertRejects( + browser.runtime.sendMessage(...args), + expectedError, + `expected error message for ${description}`); + } + + browser.test.notifyPass("sendMessage parameter validation"); + } + let extensionData = { + background, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + yield extension.awaitFinish("sendMessage parameter validation"); + + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_no_receiver.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_no_receiver.js new file mode 100644 index 000000000..f906333d2 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_no_receiver.js @@ -0,0 +1,54 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_sendMessage_without_listener() { + async function background() { + await browser.test.assertRejects( + browser.runtime.sendMessage("msg"), + "Could not establish connection. Receiving end does not exist.", + "sendMessage callback was invoked"); + + browser.test.notifyPass("sendMessage callback was invoked"); + } + let extensionData = { + background, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + yield extension.awaitFinish("sendMessage callback was invoked"); + + yield extension.unload(); +}); + +add_task(function* test_chrome_sendMessage_without_listener() { + function background() { + /* 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"); + browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage without callback"); + + 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 extensionData = { + background, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + yield extension.awaitFinish("finished chrome.runtime.sendMessage"); + + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_self.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_self.js new file mode 100644 index 000000000..e4f5e951f --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_self.js @@ -0,0 +1,51 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ + +"use strict"; + +add_task(function* test_sendMessage_to_self_should_not_trigger_onMessage() { + async function background() { + browser.runtime.onMessage.addListener(msg => { + browser.test.assertEq("msg from child", msg); + browser.test.notifyPass("sendMessage did not call same-frame onMessage"); + }); + + browser.test.onMessage.addListener(msg => { + browser.test.assertEq("sendMessage with a listener in another frame", msg); + browser.runtime.sendMessage("should only reach another frame"); + }); + + await browser.test.assertRejects( + browser.runtime.sendMessage("should not trigger same-frame onMessage"), + "Could not establish connection. Receiving end does not exist."); + + let anotherFrame = document.createElement("iframe"); + anotherFrame.src = browser.extension.getURL("extensionpage.html"); + document.body.appendChild(anotherFrame); + } + + function lastScript() { + browser.runtime.onMessage.addListener(msg => { + browser.test.assertEq("should only reach another frame", msg); + browser.runtime.sendMessage("msg from child"); + }); + browser.test.sendMessage("sendMessage callback called"); + } + + let extensionData = { + background, + files: { + "lastScript.js": lastScript, + "extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="lastScript.js"></script>`, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + + yield extension.awaitMessage("sendMessage callback called"); + extension.sendMessage("sendMessage with a listener in another frame"); + yield extension.awaitFinish("sendMessage did not call same-frame onMessage"); + + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js new file mode 100644 index 000000000..d838be5b5 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js @@ -0,0 +1,1427 @@ +"use strict"; + +Components.utils.import("resource://gre/modules/Schemas.jsm"); +Components.utils.import("resource://gre/modules/BrowserUtils.jsm"); +Components.utils.import("resource://gre/modules/ExtensionCommon.jsm"); + +let {LocalAPIImplementation, SchemaAPIInterface} = ExtensionCommon; + +let json = [ + {namespace: "testing", + + properties: { + PROP1: {value: 20}, + prop2: {type: "string"}, + prop3: { + $ref: "submodule", + }, + prop4: { + $ref: "submodule", + unsupported: true, + }, + }, + + types: [ + { + id: "type1", + type: "string", + "enum": ["value1", "value2", "value3"], + }, + + { + id: "type2", + type: "object", + properties: { + prop1: {type: "integer"}, + prop2: {type: "array", items: {"$ref": "type1"}}, + }, + }, + + { + id: "basetype1", + type: "object", + properties: { + prop1: {type: "string"}, + }, + }, + + { + id: "basetype2", + choices: [ + {type: "integer"}, + ], + }, + + { + $extend: "basetype1", + properties: { + prop2: {type: "string"}, + }, + }, + + { + $extend: "basetype2", + choices: [ + {type: "string"}, + ], + }, + + { + id: "submodule", + type: "object", + functions: [ + { + name: "sub_foo", + type: "function", + parameters: [], + returns: "integer", + }, + ], + }, + ], + + functions: [ + { + name: "foo", + type: "function", + parameters: [ + {name: "arg1", type: "integer", optional: true, default: 99}, + {name: "arg2", type: "boolean", optional: true}, + ], + }, + + { + name: "bar", + type: "function", + parameters: [ + {name: "arg1", type: "integer", optional: true}, + {name: "arg2", type: "boolean"}, + ], + }, + + { + name: "baz", + type: "function", + parameters: [ + {name: "arg1", type: "object", properties: { + prop1: {type: "string"}, + prop2: {type: "integer", optional: true}, + prop3: {type: "integer", unsupported: true}, + }}, + ], + }, + + { + name: "qux", + type: "function", + parameters: [ + {name: "arg1", "$ref": "type1"}, + ], + }, + + { + name: "quack", + type: "function", + parameters: [ + {name: "arg1", "$ref": "type2"}, + ], + }, + + { + name: "quora", + type: "function", + parameters: [ + {name: "arg1", type: "function"}, + ], + }, + + { + name: "quileute", + type: "function", + parameters: [ + {name: "arg1", type: "integer", optional: true}, + {name: "arg2", type: "integer"}, + ], + }, + + { + name: "queets", + type: "function", + unsupported: true, + parameters: [], + }, + + { + name: "quintuplets", + type: "function", + parameters: [ + {name: "obj", type: "object", properties: [], additionalProperties: {type: "integer"}}, + ], + }, + + { + name: "quasar", + type: "function", + parameters: [ + {name: "abc", type: "object", properties: { + func: {type: "function", parameters: [ + {name: "x", type: "integer"}, + ]}, + }}, + ], + }, + + { + name: "quosimodo", + type: "function", + parameters: [ + {name: "xyz", type: "object", additionalProperties: {type: "any"}}, + ], + }, + + { + name: "patternprop", + type: "function", + parameters: [ + { + name: "obj", + type: "object", + properties: {"prop1": {type: "string", pattern: "^\\d+$"}}, + patternProperties: { + "(?i)^prop\\d+$": {type: "string"}, + "^foo\\d+$": {type: "string"}, + }, + }, + ], + }, + + { + name: "pattern", + type: "function", + parameters: [ + {name: "arg", type: "string", pattern: "(?i)^[0-9a-f]+$"}, + ], + }, + + { + name: "format", + type: "function", + parameters: [ + { + name: "arg", + type: "object", + properties: { + url: {type: "string", "format": "url", "optional": true}, + relativeUrl: {type: "string", "format": "relativeUrl", "optional": true}, + strictRelativeUrl: {type: "string", "format": "strictRelativeUrl", "optional": true}, + }, + }, + ], + }, + + { + name: "formatDate", + type: "function", + parameters: [ + { + name: "arg", + type: "object", + properties: { + date: {type: "string", format: "date", optional: true}, + }, + }, + ], + }, + + { + name: "deep", + type: "function", + parameters: [ + { + name: "arg", + type: "object", + properties: { + foo: { + type: "object", + properties: { + bar: { + type: "array", + items: { + type: "object", + properties: { + baz: { + type: "object", + properties: { + required: {type: "integer"}, + optional: {type: "string", optional: true}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ], + }, + + { + name: "errors", + type: "function", + parameters: [ + { + name: "arg", + type: "object", + properties: { + warn: { + type: "string", + pattern: "^\\d+$", + optional: true, + onError: "warn", + }, + ignore: { + type: "string", + pattern: "^\\d+$", + optional: true, + onError: "ignore", + }, + default: { + type: "string", + pattern: "^\\d+$", + optional: true, + }, + }, + }, + ], + }, + + { + name: "localize", + type: "function", + parameters: [ + { + name: "arg", + type: "object", + properties: { + foo: {type: "string", "preprocess": "localize", "optional": true}, + bar: {type: "string", "optional": true}, + url: {type: "string", "preprocess": "localize", "format": "url", "optional": true}, + }, + }, + ], + }, + + { + name: "extended1", + type: "function", + parameters: [ + {name: "val", $ref: "basetype1"}, + ], + }, + + { + name: "extended2", + type: "function", + parameters: [ + {name: "val", $ref: "basetype2"}, + ], + }, + ], + + events: [ + { + name: "onFoo", + type: "function", + }, + + { + name: "onBar", + type: "function", + extraParameters: [{ + name: "filter", + type: "integer", + optional: true, + default: 1, + }], + }, + ], + }, + { + namespace: "foreign", + properties: { + foreignRef: {$ref: "testing.submodule"}, + }, + }, + { + namespace: "inject", + properties: { + PROP1: {value: "should inject"}, + }, + }, + { + namespace: "do-not-inject", + properties: { + PROP1: {value: "should not inject"}, + }, + }, +]; + +let tallied = null; + +function tally(kind, ns, name, args) { + tallied = [kind, ns, name, args]; +} + +function verify(...args) { + do_check_eq(JSON.stringify(tallied), JSON.stringify(args)); + tallied = null; +} + +let talliedErrors = []; + +function checkErrors(errors) { + do_check_eq(talliedErrors.length, errors.length, "Got expected number of errors"); + for (let [i, error] of errors.entries()) { + do_check_true(i in talliedErrors && talliedErrors[i].includes(error), + `${JSON.stringify(error)} is a substring of error ${JSON.stringify(talliedErrors[i])}`); + } + + talliedErrors.length = 0; +} + +let permissions = new Set(); + +class TallyingAPIImplementation extends SchemaAPIInterface { + constructor(namespace, name) { + super(); + this.namespace = namespace; + this.name = name; + } + + callFunction(args) { + tally("call", this.namespace, this.name, args); + } + + callFunctionNoReturn(args) { + tally("call", this.namespace, this.name, args); + } + + getProperty() { + tally("get", this.namespace, this.name); + } + + setProperty(value) { + tally("set", this.namespace, this.name, value); + } + + addListener(listener, args) { + tally("addListener", this.namespace, this.name, [listener, args]); + } + + removeListener(listener) { + tally("removeListener", this.namespace, this.name, [listener]); + } + + hasListener(listener) { + tally("hasListener", this.namespace, this.name, [listener]); + } +} + +let wrapper = { + url: "moz-extension://b66e3509-cdb3-44f6-8eb8-c8b39b3a1d27/", + + checkLoadURL(url) { + return !url.startsWith("chrome:"); + }, + + preprocessors: { + localize(value, context) { + return value.replace(/__MSG_(.*?)__/g, (m0, m1) => `${m1.toUpperCase()}`); + }, + }, + + logError(message) { + talliedErrors.push(message); + }, + + hasPermission(permission) { + return permissions.has(permission); + }, + + shouldInject(ns) { + return ns != "do-not-inject"; + }, + + getImplementation(namespace, name) { + return new TallyingAPIImplementation(namespace, name); + }, +}; + +add_task(function* () { + let url = "data:," + JSON.stringify(json); + yield Schemas.load(url); + + let root = {}; + tallied = null; + Schemas.inject(root, wrapper); + do_check_eq(tallied, null); + + do_check_eq(root.testing.PROP1, 20, "simple value property"); + do_check_eq(root.testing.type1.VALUE1, "value1", "enum type"); + do_check_eq(root.testing.type1.VALUE2, "value2", "enum type"); + + do_check_eq("inject" in root, true, "namespace 'inject' should be injected"); + do_check_eq("do-not-inject" in root, false, "namespace 'do-not-inject' should not be injected"); + + root.testing.foo(11, true); + verify("call", "testing", "foo", [11, true]); + + root.testing.foo(true); + verify("call", "testing", "foo", [99, true]); + + root.testing.foo(null, true); + verify("call", "testing", "foo", [99, true]); + + root.testing.foo(undefined, true); + verify("call", "testing", "foo", [99, true]); + + root.testing.foo(11); + verify("call", "testing", "foo", [11, null]); + + Assert.throws(() => root.testing.bar(11), + /Incorrect argument types/, + "should throw without required arg"); + + Assert.throws(() => root.testing.bar(11, true, 10), + /Incorrect argument types/, + "should throw with too many arguments"); + + root.testing.bar(true); + verify("call", "testing", "bar", [null, true]); + + root.testing.baz({prop1: "hello", prop2: 22}); + verify("call", "testing", "baz", [{prop1: "hello", prop2: 22}]); + + root.testing.baz({prop1: "hello"}); + verify("call", "testing", "baz", [{prop1: "hello", prop2: null}]); + + root.testing.baz({prop1: "hello", prop2: null}); + verify("call", "testing", "baz", [{prop1: "hello", prop2: null}]); + + Assert.throws(() => root.testing.baz({prop2: 12}), + /Property "prop1" is required/, + "should throw without required property"); + + Assert.throws(() => root.testing.baz({prop1: "hi", prop3: 12}), + /Property "prop3" is unsupported by Firefox/, + "should throw with unsupported property"); + + Assert.throws(() => root.testing.baz({prop1: "hi", prop4: 12}), + /Unexpected property "prop4"/, + "should throw with unexpected property"); + + Assert.throws(() => root.testing.baz({prop1: 12}), + /Expected string instead of 12/, + "should throw with wrong type"); + + root.testing.qux("value2"); + verify("call", "testing", "qux", ["value2"]); + + Assert.throws(() => root.testing.qux("value4"), + /Invalid enumeration value "value4"/, + "should throw for invalid enum value"); + + root.testing.quack({prop1: 12, prop2: ["value1", "value3"]}); + verify("call", "testing", "quack", [{prop1: 12, prop2: ["value1", "value3"]}]); + + Assert.throws(() => root.testing.quack({prop1: 12, prop2: ["value1", "value3", "value4"]}), + /Invalid enumeration value "value4"/, + "should throw for invalid array type"); + + function f() {} + root.testing.quora(f); + do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quora"])); + do_check_eq(tallied[3][0], f); + tallied = null; + + let g = () => 0; + root.testing.quora(g); + do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quora"])); + do_check_eq(tallied[3][0], g); + tallied = null; + + root.testing.quileute(10); + verify("call", "testing", "quileute", [null, 10]); + + Assert.throws(() => root.testing.queets(), + /queets is not a function/, + "should throw for unsupported functions"); + + root.testing.quintuplets({a: 10, b: 20, c: 30}); + verify("call", "testing", "quintuplets", [{a: 10, b: 20, c: 30}]); + + Assert.throws(() => root.testing.quintuplets({a: 10, b: 20, c: 30, d: "hi"}), + /Expected integer instead of "hi"/, + "should throw for wrong additionalProperties type"); + + root.testing.quasar({func: f}); + do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quasar"])); + do_check_eq(tallied[3][0].func, f); + tallied = null; + + root.testing.quosimodo({a: 10, b: 20, c: 30}); + verify("call", "testing", "quosimodo", [{a: 10, b: 20, c: 30}]); + tallied = null; + + Assert.throws(() => root.testing.quosimodo(10), + /Incorrect argument types/, + "should throw for wrong type"); + + root.testing.patternprop({prop1: "12", prop2: "42", Prop3: "43", foo1: "x"}); + verify("call", "testing", "patternprop", [{prop1: "12", prop2: "42", Prop3: "43", foo1: "x"}]); + tallied = null; + + root.testing.patternprop({prop1: "12"}); + verify("call", "testing", "patternprop", [{prop1: "12"}]); + tallied = null; + + Assert.throws(() => root.testing.patternprop({prop1: "12", foo1: null}), + /Expected string instead of null/, + "should throw for wrong property type"); + + Assert.throws(() => root.testing.patternprop({prop1: "xx", prop2: "yy"}), + /String "xx" must match \/\^\\d\+\$\//, + "should throw for wrong property type"); + + Assert.throws(() => root.testing.patternprop({prop1: "12", prop2: 42}), + /Expected string instead of 42/, + "should throw for wrong property type"); + + Assert.throws(() => root.testing.patternprop({prop1: "12", prop2: null}), + /Expected string instead of null/, + "should throw for wrong property type"); + + Assert.throws(() => root.testing.patternprop({prop1: "12", propx: "42"}), + /Unexpected property "propx"/, + "should throw for unexpected property"); + + Assert.throws(() => root.testing.patternprop({prop1: "12", Foo1: "x"}), + /Unexpected property "Foo1"/, + "should throw for unexpected property"); + + root.testing.pattern("DEADbeef"); + verify("call", "testing", "pattern", ["DEADbeef"]); + tallied = null; + + Assert.throws(() => root.testing.pattern("DEADcow"), + /String "DEADcow" must match \/\^\[0-9a-f\]\+\$\/i/, + "should throw for non-match"); + + root.testing.format({url: "http://foo/bar", + relativeUrl: "http://foo/bar"}); + verify("call", "testing", "format", [{url: "http://foo/bar", + relativeUrl: "http://foo/bar", + strictRelativeUrl: null}]); + tallied = null; + + root.testing.format({relativeUrl: "foo.html", strictRelativeUrl: "foo.html"}); + verify("call", "testing", "format", [{url: null, + relativeUrl: `${wrapper.url}foo.html`, + strictRelativeUrl: `${wrapper.url}foo.html`}]); + tallied = null; + + for (let format of ["url", "relativeUrl"]) { + Assert.throws(() => root.testing.format({[format]: "chrome://foo/content/"}), + /Access denied/, + "should throw for access denied"); + } + + for (let urlString of ["//foo.html", "http://foo/bar.html"]) { + Assert.throws(() => root.testing.format({strictRelativeUrl: urlString}), + /must be a relative URL/, + "should throw for non-relative URL"); + } + + const dates = [ + "2016-03-04", + "2016-03-04T08:00:00Z", + "2016-03-04T08:00:00.000Z", + "2016-03-04T08:00:00-08:00", + "2016-03-04T08:00:00.000-08:00", + "2016-03-04T08:00:00+08:00", + "2016-03-04T08:00:00.000+08:00", + "2016-03-04T08:00:00+0800", + "2016-03-04T08:00:00-0800", + ]; + dates.forEach(str => { + root.testing.formatDate({date: str}); + verify("call", "testing", "formatDate", [{date: str}]); + }); + + // Make sure that a trivial change to a valid date invalidates it. + dates.forEach(str => { + Assert.throws(() => root.testing.formatDate({date: "0" + str}), + /Invalid date string/, + "should throw for invalid iso date string"); + Assert.throws(() => root.testing.formatDate({date: str + "0"}), + /Invalid date string/, + "should throw for invalid iso date string"); + }); + + const badDates = [ + "I do not look anything like a date string", + "2016-99-99", + "2016-03-04T25:00:00Z", + ]; + badDates.forEach(str => { + Assert.throws(() => root.testing.formatDate({date: str}), + /Invalid date string/, + "should throw for invalid iso date string"); + }); + + root.testing.deep({foo: {bar: [{baz: {required: 12, optional: "42"}}]}}); + verify("call", "testing", "deep", [{foo: {bar: [{baz: {required: 12, optional: "42"}}]}}]); + tallied = null; + + Assert.throws(() => root.testing.deep({foo: {bar: [{baz: {optional: "42"}}]}}), + /Type error for parameter arg \(Error processing foo\.bar\.0\.baz: Property "required" is required\) for testing\.deep/, + "should throw with the correct object path"); + + Assert.throws(() => root.testing.deep({foo: {bar: [{baz: {required: 12, optional: 42}}]}}), + /Type error for parameter arg \(Error processing foo\.bar\.0\.baz\.optional: Expected string instead of 42\) for testing\.deep/, + "should throw with the correct object path"); + + + talliedErrors.length = 0; + + root.testing.errors({warn: "0123", ignore: "0123", default: "0123"}); + verify("call", "testing", "errors", [{warn: "0123", ignore: "0123", default: "0123"}]); + checkErrors([]); + + root.testing.errors({warn: "0123", ignore: "x123", default: "0123"}); + verify("call", "testing", "errors", [{warn: "0123", ignore: null, default: "0123"}]); + checkErrors([]); + + root.testing.errors({warn: "x123", ignore: "0123", default: "0123"}); + verify("call", "testing", "errors", [{warn: null, ignore: "0123", default: "0123"}]); + checkErrors([ + 'String "x123" must match /^\\d+$/', + ]); + + + root.testing.onFoo.addListener(f); + do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onFoo"])); + do_check_eq(tallied[3][0], f); + do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([])); + tallied = null; + + root.testing.onFoo.removeListener(f); + do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["removeListener", "testing", "onFoo"])); + do_check_eq(tallied[3][0], f); + tallied = null; + + root.testing.onFoo.hasListener(f); + do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["hasListener", "testing", "onFoo"])); + do_check_eq(tallied[3][0], f); + tallied = null; + + Assert.throws(() => root.testing.onFoo.addListener(10), + /Invalid listener/, + "addListener with non-function should throw"); + + root.testing.onBar.addListener(f, 10); + do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onBar"])); + do_check_eq(tallied[3][0], f); + do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([10])); + tallied = null; + + root.testing.onBar.addListener(f); + do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onBar"])); + do_check_eq(tallied[3][0], f); + do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([1])); + tallied = null; + + Assert.throws(() => root.testing.onBar.addListener(f, "hi"), + /Incorrect argument types/, + "addListener with wrong extra parameter should throw"); + + let target = {prop1: 12, prop2: ["value1", "value3"]}; + let proxy = new Proxy(target, {}); + Assert.throws(() => root.testing.quack(proxy), + /Expected a plain JavaScript object, got a Proxy/, + "should throw when passing a Proxy"); + + if (Symbol.toStringTag) { + let stringTarget = {prop1: 12, prop2: ["value1", "value3"]}; + stringTarget[Symbol.toStringTag] = () => "[object Object]"; + let stringProxy = new Proxy(stringTarget, {}); + Assert.throws(() => root.testing.quack(stringProxy), + /Expected a plain JavaScript object, got a Proxy/, + "should throw when passing a Proxy"); + } + + + root.testing.localize({foo: "__MSG_foo__", bar: "__MSG_foo__", url: "__MSG_http://example.com/__"}); + verify("call", "testing", "localize", [{foo: "FOO", bar: "__MSG_foo__", url: "http://example.com/"}]); + tallied = null; + + + Assert.throws(() => root.testing.localize({url: "__MSG_/foo/bar__"}), + /\/FOO\/BAR is not a valid URL\./, + "should throw for invalid URL"); + + + root.testing.extended1({prop1: "foo", prop2: "bar"}); + verify("call", "testing", "extended1", [{prop1: "foo", prop2: "bar"}]); + tallied = null; + + Assert.throws(() => root.testing.extended1({prop1: "foo", prop2: 12}), + /Expected string instead of 12/, + "should throw for wrong property type"); + + Assert.throws(() => root.testing.extended1({prop1: "foo"}), + /Property "prop2" is required/, + "should throw for missing property"); + + Assert.throws(() => root.testing.extended1({prop1: "foo", prop2: "bar", prop3: "xxx"}), + /Unexpected property "prop3"/, + "should throw for extra property"); + + + root.testing.extended2("foo"); + verify("call", "testing", "extended2", ["foo"]); + tallied = null; + + root.testing.extended2(12); + verify("call", "testing", "extended2", [12]); + tallied = null; + + Assert.throws(() => root.testing.extended2(true), + /Incorrect argument types/, + "should throw for wrong argument type"); + + root.testing.prop3.sub_foo(); + verify("call", "testing.prop3", "sub_foo", []); + tallied = null; + + Assert.throws(() => root.testing.prop4.sub_foo(), + /root.testing.prop4 is undefined/, + "should throw for unsupported submodule"); + + root.foreign.foreignRef.sub_foo(); + verify("call", "foreign.foreignRef", "sub_foo", []); + tallied = null; +}); + +let deprecatedJson = [ + {namespace: "deprecated", + + properties: { + accessor: { + type: "string", + writable: true, + deprecated: "This is not the property you are looking for", + }, + }, + + types: [ + { + "id": "Type", + "type": "string", + }, + ], + + functions: [ + { + name: "property", + type: "function", + parameters: [ + { + name: "arg", + type: "object", + properties: { + foo: { + type: "string", + }, + }, + additionalProperties: { + type: "any", + deprecated: "Unknown property", + }, + }, + ], + }, + + { + name: "value", + type: "function", + parameters: [ + { + name: "arg", + choices: [ + { + type: "integer", + }, + { + type: "string", + deprecated: "Please use an integer, not ${value}", + }, + ], + }, + ], + }, + + { + name: "choices", + type: "function", + parameters: [ + { + name: "arg", + deprecated: "You have no choices", + choices: [ + { + type: "integer", + }, + ], + }, + ], + }, + + { + name: "ref", + type: "function", + parameters: [ + { + name: "arg", + choices: [ + { + $ref: "Type", + deprecated: "Deprecated alias", + }, + ], + }, + ], + }, + + { + name: "method", + type: "function", + deprecated: "Do not call this method", + parameters: [ + ], + }, + ], + + events: [ + { + name: "onDeprecated", + type: "function", + deprecated: "This event does not work", + }, + ], + }, +]; + +add_task(function* testDeprecation() { + let url = "data:," + JSON.stringify(deprecatedJson); + yield Schemas.load(url); + + let root = {}; + Schemas.inject(root, wrapper); + + talliedErrors.length = 0; + + + root.deprecated.property({foo: "bar", xxx: "any", yyy: "property"}); + verify("call", "deprecated", "property", [{foo: "bar", xxx: "any", yyy: "property"}]); + checkErrors([ + "Error processing xxx: Unknown property", + "Error processing yyy: Unknown property", + ]); + + root.deprecated.value(12); + verify("call", "deprecated", "value", [12]); + checkErrors([]); + + root.deprecated.value("12"); + verify("call", "deprecated", "value", ["12"]); + checkErrors(["Please use an integer, not \"12\""]); + + root.deprecated.choices(12); + verify("call", "deprecated", "choices", [12]); + checkErrors(["You have no choices"]); + + root.deprecated.ref("12"); + verify("call", "deprecated", "ref", ["12"]); + checkErrors(["Deprecated alias"]); + + root.deprecated.method(); + verify("call", "deprecated", "method", []); + checkErrors(["Do not call this method"]); + + + void root.deprecated.accessor; + verify("get", "deprecated", "accessor", null); + checkErrors(["This is not the property you are looking for"]); + + root.deprecated.accessor = "x"; + verify("set", "deprecated", "accessor", "x"); + checkErrors(["This is not the property you are looking for"]); + + + root.deprecated.onDeprecated.addListener(() => {}); + checkErrors(["This event does not work"]); + + root.deprecated.onDeprecated.removeListener(() => {}); + checkErrors(["This event does not work"]); + + root.deprecated.onDeprecated.hasListener(() => {}); + checkErrors(["This event does not work"]); +}); + + +let choicesJson = [ + {namespace: "choices", + + types: [ + ], + + functions: [ + { + name: "meh", + type: "function", + parameters: [ + { + name: "arg", + choices: [ + { + type: "string", + enum: ["foo", "bar", "baz"], + }, + { + type: "string", + pattern: "florg.*meh", + }, + { + type: "integer", + minimum: 12, + maximum: 42, + }, + ], + }, + ], + }, + + { + name: "foo", + type: "function", + parameters: [ + { + name: "arg", + choices: [ + { + type: "object", + properties: { + blurg: { + type: "string", + unsupported: true, + optional: true, + }, + }, + additionalProperties: { + type: "string", + }, + }, + { + type: "string", + }, + { + type: "array", + minItems: 2, + maxItems: 3, + items: { + type: "integer", + }, + }, + ], + }, + ], + }, + + { + name: "bar", + type: "function", + parameters: [ + { + name: "arg", + choices: [ + { + type: "object", + properties: { + baz: { + type: "string", + }, + }, + }, + { + type: "array", + items: { + type: "integer", + }, + }, + ], + }, + ], + }, + ]}, +]; + +add_task(function* testChoices() { + let url = "data:," + JSON.stringify(choicesJson); + yield Schemas.load(url); + + let root = {}; + Schemas.inject(root, wrapper); + + talliedErrors.length = 0; + + Assert.throws(() => root.choices.meh("frog"), + /Value must either: be one of \["foo", "bar", "baz"\], match the pattern \/florg\.\*meh\/, or be an integer value/); + + Assert.throws(() => root.choices.meh(4), + /be a string value, or be at least 12/); + + Assert.throws(() => root.choices.meh(43), + /be a string value, or be no greater than 42/); + + + Assert.throws(() => root.choices.foo([]), + /be an object value, be a string value, or have at least 2 items/); + + Assert.throws(() => root.choices.foo([1, 2, 3, 4]), + /be an object value, be a string value, or have at most 3 items/); + + Assert.throws(() => root.choices.foo({foo: 12}), + /.foo must be a string value, be a string value, or be an array value/); + + Assert.throws(() => root.choices.foo({blurg: "foo"}), + /not contain an unsupported "blurg" property, be a string value, or be an array value/); + + + Assert.throws(() => root.choices.bar({}), + /contain the required "baz" property, or be an array value/); + + Assert.throws(() => root.choices.bar({baz: "x", quux: "y"}), + /not contain an unexpected "quux" property, or be an array value/); + + Assert.throws(() => root.choices.bar({baz: "x", quux: "y", foo: "z"}), + /not contain the unexpected properties \[foo, quux\], or be an array value/); +}); + + +let permissionsJson = [ + {namespace: "noPerms", + + types: [], + + functions: [ + { + name: "noPerms", + type: "function", + parameters: [], + }, + + { + name: "fooPerm", + type: "function", + permissions: ["foo"], + parameters: [], + }, + ]}, + + {namespace: "fooPerm", + + permissions: ["foo"], + + types: [], + + functions: [ + { + name: "noPerms", + type: "function", + parameters: [], + }, + + { + name: "fooBarPerm", + type: "function", + permissions: ["foo.bar"], + parameters: [], + }, + ]}, +]; + +add_task(function* testPermissions() { + let url = "data:," + JSON.stringify(permissionsJson); + yield Schemas.load(url); + + let root = {}; + Schemas.inject(root, wrapper); + + equal(typeof root.noPerms, "object", "noPerms namespace should exist"); + equal(typeof root.noPerms.noPerms, "function", "noPerms.noPerms method should exist"); + + ok(!("fooPerm" in root.noPerms), "noPerms.fooPerm should not method exist"); + + ok(!("fooPerm" in root), "fooPerm namespace should not exist"); + + + do_print('Add "foo" permission'); + permissions.add("foo"); + + root = {}; + Schemas.inject(root, wrapper); + + equal(typeof root.noPerms, "object", "noPerms namespace should exist"); + equal(typeof root.noPerms.noPerms, "function", "noPerms.noPerms method should exist"); + equal(typeof root.noPerms.fooPerm, "function", "noPerms.fooPerm method should exist"); + + equal(typeof root.fooPerm, "object", "fooPerm namespace should exist"); + equal(typeof root.fooPerm.noPerms, "function", "noPerms.noPerms method should exist"); + + ok(!("fooBarPerm" in root.fooPerm), "fooPerm.fooBarPerm method should not exist"); + + + do_print('Add "foo.bar" permission'); + permissions.add("foo.bar"); + + root = {}; + Schemas.inject(root, wrapper); + + equal(typeof root.noPerms, "object", "noPerms namespace should exist"); + equal(typeof root.noPerms.noPerms, "function", "noPerms.noPerms method should exist"); + equal(typeof root.noPerms.fooPerm, "function", "noPerms.fooPerm method should exist"); + + equal(typeof root.fooPerm, "object", "fooPerm namespace should exist"); + equal(typeof root.fooPerm.noPerms, "function", "noPerms.noPerms method should exist"); + equal(typeof root.fooPerm.fooBarPerm, "function", "noPerms.fooBarPerm method should exist"); +}); + +let nestedNamespaceJson = [ + { + "namespace": "nested.namespace", + "types": [ + { + "id": "CustomType", + "type": "object", + "events": [ + { + "name": "onEvent", + }, + ], + "properties": { + "url": { + "type": "string", + }, + }, + "functions": [ + { + "name": "functionOnCustomType", + "type": "function", + "parameters": [ + { + "name": "title", + "type": "string", + }, + ], + }, + ], + }, + ], + "properties": { + "instanceOfCustomType": { + "$ref": "CustomType", + }, + }, + "functions": [ + { + "name": "create", + "type": "function", + "parameters": [ + { + "name": "title", + "type": "string", + }, + ], + }, + ], + }, +]; + +add_task(function* testNestedNamespace() { + let url = "data:," + JSON.stringify(nestedNamespaceJson); + + yield Schemas.load(url); + + let root = {}; + Schemas.inject(root, wrapper); + + talliedErrors.length = 0; + + ok(root.nested, "The root object contains the first namespace level"); + ok(root.nested.namespace, "The first level object contains the second namespace level"); + + ok(root.nested.namespace.create, "Got the expected function in the nested namespace"); + do_check_eq(typeof root.nested.namespace.create, "function", + "The property is a function as expected"); + + let {instanceOfCustomType} = root.nested.namespace; + + ok(instanceOfCustomType, + "Got the expected instance of the CustomType defined in the schema"); + ok(instanceOfCustomType.functionOnCustomType, + "Got the expected method in the CustomType instance"); + + // TODO: test support events and properties in a SubModuleType defined in the schema, + // once implemented, e.g.: + // + // ok(instanceOfCustomType.url, + // "Got the expected property defined in the CustomType instance) + // + // ok(instanceOfCustomType.onEvent && + // instanceOfCustomType.onEvent.addListener && + // typeof instanceOfCustomType.onEvent.addListener == "function", + // "Got the expected event defined in the CustomType instance"); +}); + +add_task(function* testLocalAPIImplementation() { + let countGet2 = 0; + let countProp3 = 0; + let countProp3SubFoo = 0; + + let testingApiObj = { + get PROP1() { + // PROP1 is a schema-defined constant. + throw new Error("Unexpected get PROP1"); + }, + get prop2() { + ++countGet2; + return "prop2 val"; + }, + get prop3() { + throw new Error("Unexpected get prop3"); + }, + set prop3(v) { + // prop3 is a submodule, defined as a function, so the API should not pass + // through assignment to prop3. + throw new Error("Unexpected set prop3"); + }, + }; + let submoduleApiObj = { + get sub_foo() { + ++countProp3; + return () => { + return ++countProp3SubFoo; + }; + }, + }; + + let localWrapper = { + shouldInject(ns) { + return ns == "testing" || ns == "testing.prop3"; + }, + getImplementation(ns, name) { + do_check_true(ns == "testing" || ns == "testing.prop3"); + if (ns == "testing.prop3" && name == "sub_foo") { + // It is fine to use `null` here because we don't call async functions. + return new LocalAPIImplementation(submoduleApiObj, name, null); + } + // It is fine to use `null` here because we don't call async functions. + return new LocalAPIImplementation(testingApiObj, name, null); + }, + }; + + let root = {}; + Schemas.inject(root, localWrapper); + do_check_eq(countGet2, 0); + do_check_eq(countProp3, 0); + do_check_eq(countProp3SubFoo, 0); + + do_check_eq(root.testing.PROP1, 20); + + do_check_eq(root.testing.prop2, "prop2 val"); + do_check_eq(countGet2, 1); + + do_check_eq(root.testing.prop2, "prop2 val"); + do_check_eq(countGet2, 2); + + do_print(JSON.stringify(root.testing)); + do_check_eq(root.testing.prop3.sub_foo(), 1); + do_check_eq(countProp3, 1); + do_check_eq(countProp3SubFoo, 1); + + do_check_eq(root.testing.prop3.sub_foo(), 2); + do_check_eq(countProp3, 2); + do_check_eq(countProp3SubFoo, 2); + + root.testing.prop3.sub_foo = () => { return "overwritten"; }; + do_check_eq(root.testing.prop3.sub_foo(), "overwritten"); + + root.testing.prop3 = {sub_foo() { return "overwritten again"; }}; + do_check_eq(root.testing.prop3.sub_foo(), "overwritten again"); + do_check_eq(countProp3SubFoo, 2); +}); + + +let defaultsJson = [ + {namespace: "defaultsJson", + + types: [], + + functions: [ + { + name: "defaultFoo", + type: "function", + parameters: [ + {name: "arg", type: "object", optional: true, properties: { + prop1: {type: "integer", optional: true}, + }, default: {prop1: 1}}, + ], + returns: { + type: "object", + }, + }, + ]}, +]; + +add_task(function* testDefaults() { + let url = "data:," + JSON.stringify(defaultsJson); + yield Schemas.load(url); + + let testingApiObj = { + defaultFoo: function(arg) { + if (Object.keys(arg) != "prop1") { + throw new Error(`Received the expected default object, default: ${JSON.stringify(arg)}`); + } + arg.newProp = 1; + return arg; + }, + }; + + let localWrapper = { + shouldInject(ns) { + return true; + }, + getImplementation(ns, name) { + return new LocalAPIImplementation(testingApiObj, name, null); + }, + }; + + let root = {}; + Schemas.inject(root, localWrapper); + + deepEqual(root.defaultsJson.defaultFoo(), {prop1: 1, newProp: 1}); + deepEqual(root.defaultsJson.defaultFoo({prop1: 2}), {prop1: 2, newProp: 1}); + deepEqual(root.defaultsJson.defaultFoo(), {prop1: 1, newProp: 1}); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_allowed_contexts.js b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_allowed_contexts.js new file mode 100644 index 000000000..606459764 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_allowed_contexts.js @@ -0,0 +1,147 @@ +"use strict"; + +Components.utils.import("resource://gre/modules/Schemas.jsm"); + +let schemaJson = [ + { + namespace: "noAllowedContexts", + properties: { + prop1: {type: "object"}, + prop2: {type: "object", allowedContexts: ["test_zero", "test_one"]}, + prop3: {type: "number", value: 1}, + prop4: {type: "number", value: 1, allowedContexts: ["numeric_one"]}, + }, + }, + { + namespace: "defaultContexts", + defaultContexts: ["test_two"], + properties: { + prop1: {type: "object"}, + prop2: {type: "object", allowedContexts: ["test_three"]}, + prop3: {type: "number", value: 1}, + prop4: {type: "number", value: 1, allowedContexts: ["numeric_two"]}, + }, + }, + { + namespace: "withAllowedContexts", + allowedContexts: ["test_four"], + properties: { + prop1: {type: "object"}, + prop2: {type: "object", allowedContexts: ["test_five"]}, + prop3: {type: "number", value: 1}, + prop4: {type: "number", value: 1, allowedContexts: ["numeric_three"]}, + }, + }, + { + namespace: "withAllowedContextsAndDefault", + allowedContexts: ["test_six"], + defaultContexts: ["test_seven"], + properties: { + prop1: {type: "object"}, + prop2: {type: "object", allowedContexts: ["test_eight"]}, + prop3: {type: "number", value: 1}, + prop4: {type: "number", value: 1, allowedContexts: ["numeric_four"]}, + }, + }, + { + namespace: "with_submodule", + defaultContexts: ["test_nine"], + types: [{ + id: "subtype", + type: "object", + functions: [{ + name: "noAllowedContexts", + type: "function", + parameters: [], + }, { + name: "allowedContexts", + allowedContexts: ["test_ten"], + type: "function", + parameters: [], + }], + }], + properties: { + prop1: {$ref: "subtype"}, + prop2: {$ref: "subtype", allowedContexts: ["test_eleven"]}, + }, + }, +]; +add_task(function* testRestrictions() { + let url = "data:," + JSON.stringify(schemaJson); + yield Schemas.load(url); + let results = {}; + let localWrapper = { + shouldInject(ns, name, allowedContexts) { + name = name === null ? ns : ns + "." + name; + results[name] = allowedContexts.join(","); + return true; + }, + getImplementation() { + // The actual implementation is not significant for this test. + // Let's take this opportunity to see if schema generation is free of + // exceptions even when somehow getImplementation does not return an + // implementation. + }, + }; + + let root = {}; + Schemas.inject(root, localWrapper); + + function verify(path, expected) { + let obj = root; + for (let thing of path.split(".")) { + try { + obj = obj[thing]; + } catch (e) { + // Blech. + } + } + + let result = results[path]; + equal(result, expected); + } + + verify("noAllowedContexts", ""); + verify("noAllowedContexts.prop1", ""); + verify("noAllowedContexts.prop2", "test_zero,test_one"); + verify("noAllowedContexts.prop3", ""); + verify("noAllowedContexts.prop4", "numeric_one"); + + verify("defaultContexts", ""); + verify("defaultContexts.prop1", "test_two"); + verify("defaultContexts.prop2", "test_three"); + verify("defaultContexts.prop3", "test_two"); + verify("defaultContexts.prop4", "numeric_two"); + + verify("withAllowedContexts", "test_four"); + verify("withAllowedContexts.prop1", ""); + verify("withAllowedContexts.prop2", "test_five"); + verify("withAllowedContexts.prop3", ""); + verify("withAllowedContexts.prop4", "numeric_three"); + + verify("withAllowedContextsAndDefault", "test_six"); + verify("withAllowedContextsAndDefault.prop1", "test_seven"); + verify("withAllowedContextsAndDefault.prop2", "test_eight"); + verify("withAllowedContextsAndDefault.prop3", "test_seven"); + verify("withAllowedContextsAndDefault.prop4", "numeric_four"); + + verify("with_submodule", ""); + verify("with_submodule.prop1", "test_nine"); + verify("with_submodule.prop1.noAllowedContexts", "test_nine"); + verify("with_submodule.prop1.allowedContexts", "test_ten"); + verify("with_submodule.prop2", "test_eleven"); + // Note: test_nine inherits allowed contexts from the namespace, not from + // submodule. There is no "defaultContexts" for submodule types to not + // complicate things. + verify("with_submodule.prop1.noAllowedContexts", "test_nine"); + verify("with_submodule.prop1.allowedContexts", "test_ten"); + + // This is a constant, so it does not matter that getImplementation does not + // return an implementation since the API injector should take care of it. + equal(root.noAllowedContexts.prop3, 1); + + Assert.throws(() => root.noAllowedContexts.prop1, + /undefined/, + "Should throw when the implementation is absent."); +}); + diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js new file mode 100644 index 000000000..36d88d722 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js @@ -0,0 +1,102 @@ +"use strict"; + +Components.utils.import("resource://gre/modules/ExtensionCommon.jsm"); +Components.utils.import("resource://gre/modules/Schemas.jsm"); + +let { + BaseContext, + SchemaAPIManager, +} = ExtensionCommon; + +let nestedNamespaceJson = [ + { + "namespace": "backgroundAPI.testnamespace", + "functions": [ + { + "name": "create", + "type": "function", + "parameters": [ + { + "name": "title", + "type": "string", + }, + ], + "returns": { + "type": "string", + }, + }, + ], + }, + { + "namespace": "noBackgroundAPI.testnamespace", + "functions": [ + { + "name": "create", + "type": "function", + "parameters": [ + { + "name": "title", + "type": "string", + }, + ], + }, + ], + }, +]; + +let global = this; +class StubContext extends BaseContext { + constructor() { + let fakeExtension = {id: "test@web.extension"}; + super("addon_child", fakeExtension); + this.sandbox = Cu.Sandbox(global); + this.viewType = "background"; + } + + get cloneScope() { + return this.sandbox; + } +} + +add_task(function* testSchemaAPIInjection() { + let url = "data:," + JSON.stringify(nestedNamespaceJson); + + // Load the schema of the fake APIs. + yield Schemas.load(url); + + let apiManager = new SchemaAPIManager("addon"); + + // Register an API that will skip the background page. + apiManager.registerSchemaAPI("noBackgroundAPI.testnamespace", "addon_child", context => { + // This API should not be available in this context, return null so that + // the schema wrapper is removed as well. + return null; + }); + + // Register an API that will skip any but the background page. + apiManager.registerSchemaAPI("backgroundAPI.testnamespace", "addon_child", context => { + if (context.viewType === "background") { + return { + backgroundAPI: { + testnamespace: { + create(title) { + return title; + }, + }, + }, + }; + } + + // This API should not be available in this context, return null so that + // the schema wrapper is removed as well. + return null; + }); + + let context = new StubContext(); + let browserObj = {}; + apiManager.generateAPIs(context, browserObj); + + do_check_eq(browserObj.noBackgroundAPI, undefined); + const res = browserObj.backgroundAPI.testnamespace.create("param-value"); + do_check_eq(res, "param-value"); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js new file mode 100644 index 000000000..6397d1f96 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js @@ -0,0 +1,232 @@ +"use strict"; + +Components.utils.import("resource://gre/modules/ExtensionCommon.jsm"); +Components.utils.import("resource://gre/modules/Schemas.jsm"); + +let {BaseContext, LocalAPIImplementation} = ExtensionCommon; + +let schemaJson = [ + { + namespace: "testnamespace", + functions: [{ + name: "one_required", + type: "function", + parameters: [{ + name: "first", + type: "function", + parameters: [], + }], + }, { + name: "one_optional", + type: "function", + parameters: [{ + name: "first", + type: "function", + parameters: [], + optional: true, + }], + }, { + name: "async_required", + type: "function", + async: "first", + parameters: [{ + name: "first", + type: "function", + parameters: [], + }], + }, { + name: "async_optional", + type: "function", + async: "first", + parameters: [{ + name: "first", + type: "function", + parameters: [], + optional: true, + }], + }], + }, +]; + +const global = this; +class StubContext extends BaseContext { + constructor() { + let fakeExtension = {id: "test@web.extension"}; + super("testEnv", fakeExtension); + this.sandbox = Cu.Sandbox(global); + } + + get cloneScope() { + return this.sandbox; + } + + get principal() { + return Cu.getObjectPrincipal(this.sandbox); + } +} + +let context; + +function generateAPIs(extraWrapper, apiObj) { + context = new StubContext(); + let localWrapper = { + shouldInject() { + return true; + }, + getImplementation(namespace, name) { + return new LocalAPIImplementation(apiObj, name, context); + }, + }; + Object.assign(localWrapper, extraWrapper); + + let root = {}; + Schemas.inject(root, localWrapper); + return root.testnamespace; +} + +add_task(function* testParameterValidation() { + yield Schemas.load("data:," + JSON.stringify(schemaJson)); + + let testnamespace; + function assertThrows(name, ...args) { + Assert.throws(() => testnamespace[name](...args), + /Incorrect argument types/, + `Expected testnamespace.${name}(${args.map(String).join(", ")}) to throw.`); + } + function assertNoThrows(name, ...args) { + try { + testnamespace[name](...args); + } catch (e) { + do_print(`testnamespace.${name}(${args.map(String).join(", ")}) unexpectedly threw.`); + throw new Error(e); + } + } + let cb = () => {}; + + for (let isChromeCompat of [true, false]) { + do_print(`Testing API validation with isChromeCompat=${isChromeCompat}`); + testnamespace = generateAPIs({ + isChromeCompat, + }, { + one_required() {}, + one_optional() {}, + async_required() {}, + async_optional() {}, + }); + + assertThrows("one_required"); + assertThrows("one_required", null); + assertNoThrows("one_required", cb); + assertThrows("one_required", cb, null); + assertThrows("one_required", cb, cb); + + assertNoThrows("one_optional"); + assertNoThrows("one_optional", null); + assertNoThrows("one_optional", cb); + assertThrows("one_optional", cb, null); + assertThrows("one_optional", cb, cb); + + // Schema-based validation happens before an async method is called, so + // errors should be thrown synchronously. + + // The parameter was declared as required, but there was also an "async" + // attribute with the same value as the parameter name, so the callback + // parameter is actually optional. + assertNoThrows("async_required"); + assertNoThrows("async_required", null); + assertNoThrows("async_required", cb); + assertThrows("async_required", cb, null); + assertThrows("async_required", cb, cb); + + assertNoThrows("async_optional"); + assertNoThrows("async_optional", null); + assertNoThrows("async_optional", cb); + assertThrows("async_optional", cb, null); + assertThrows("async_optional", cb, cb); + } +}); + +add_task(function* testAsyncResults() { + yield Schemas.load("data:," + JSON.stringify(schemaJson)); + function* runWithCallback(func) { + do_print(`Calling testnamespace.${func.name}, expecting callback with result`); + return yield new Promise(resolve => { + let result = "uninitialized value"; + let returnValue = func(reply => { + result = reply; + resolve(result); + }); + // When a callback is given, the return value must be missing. + do_check_eq(returnValue, undefined); + // Callback must be called asynchronously. + do_check_eq(result, "uninitialized value"); + }); + } + + function* runFailCallback(func) { + do_print(`Calling testnamespace.${func.name}, expecting callback with error`); + return yield new Promise(resolve => { + func(reply => { + do_check_eq(reply, undefined); + resolve(context.lastError.message); // eslint-disable-line no-undef + }); + }); + } + + for (let isChromeCompat of [true, false]) { + do_print(`Testing API invocation with isChromeCompat=${isChromeCompat}`); + let testnamespace = generateAPIs({ + isChromeCompat, + }, { + async_required(cb) { + do_check_eq(cb, undefined); + return Promise.resolve(1); + }, + async_optional(cb) { + do_check_eq(cb, undefined); + return Promise.resolve(2); + }, + }); + if (!isChromeCompat) { // No promises for chrome. + do_print("testnamespace.async_required should be a Promise"); + let promise = testnamespace.async_required(); + do_check_true(promise instanceof context.cloneScope.Promise); + do_check_eq(yield promise, 1); + + do_print("testnamespace.async_optional should be a Promise"); + promise = testnamespace.async_optional(); + do_check_true(promise instanceof context.cloneScope.Promise); + do_check_eq(yield promise, 2); + } + + do_check_eq(yield* runWithCallback(testnamespace.async_required), 1); + do_check_eq(yield* runWithCallback(testnamespace.async_optional), 2); + + let otherSandbox = Cu.Sandbox(null, {}); + let errorFactories = [ + msg => { throw new context.cloneScope.Error(msg); }, + msg => context.cloneScope.Promise.reject({message: msg}), + msg => Cu.evalInSandbox(`throw new Error("${msg}")`, otherSandbox), + msg => Cu.evalInSandbox(`Promise.reject({message: "${msg}"})`, otherSandbox), + ]; + for (let makeError of errorFactories) { + do_print(`Testing callback/promise with error caused by: ${makeError}`); + testnamespace = generateAPIs({ + isChromeCompat, + }, { + async_required() { return makeError("ONE"); }, + async_optional() { return makeError("TWO"); }, + }); + + if (!isChromeCompat) { // No promises for chrome. + yield Assert.rejects(testnamespace.async_required(), /ONE/, + "should reject testnamespace.async_required()").catch(() => {}); + yield Assert.rejects(testnamespace.async_optional(), /TWO/, + "should reject testnamespace.async_optional()").catch(() => {}); + } + + do_check_eq(yield* runFailCallback(testnamespace.async_required), "ONE"); + do_check_eq(yield* runFailCallback(testnamespace.async_optional), "TWO"); + } + } +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_simple.js b/toolkit/components/extensions/test/xpcshell/test_ext_simple.js new file mode 100644 index 000000000..91b10354c --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_simple.js @@ -0,0 +1,69 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* test_simple() { + let extensionData = { + manifest: { + "name": "Simple extension test", + "version": "1.0", + "manifest_version": 2, + "description": "", + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + yield extension.unload(); +}); + +add_task(function* test_background() { + 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, + manifest: { + "name": "Simple extension test", + "version": "1.0", + "manifest_version": 2, + "description": "", + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + let [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]); + equal(x, 1, "got correct value from extension"); + + extension.sendMessage(10, 20); + yield extension.awaitFinish(); + yield extension.unload(); +}); + +add_task(function* test_extensionTypes() { + let extensionData = { + background: function() { + browser.test.assertEq(typeof browser.extensionTypes, "object", "browser.extensionTypes exists"); + browser.test.assertEq(typeof browser.extensionTypes.RunAt, "object", "browser.extensionTypes.RunAt exists"); + browser.test.notifyPass("extentionTypes test passed"); + }, + }; + + let extension = ExtensionTestUtils.loadExtension(extensionData); + + yield extension.startup(); + yield extension.awaitFinish(); + yield extension.unload(); +}); + diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_storage.js b/toolkit/components/extensions/test/xpcshell/test_ext_storage.js new file mode 100644 index 000000000..df46dfb63 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage.js @@ -0,0 +1,334 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const STORAGE_SYNC_PREF = "webextensions.storage.sync.enabled"; +Cu.import("resource://gre/modules/Preferences.jsm"); + +/** + * Utility function to ensure that all supported APIs for getting are + * tested. + * + * @param {string} areaName + * either "local" or "sync" according to what we want to test + * @param {string} prop + * "key" to look up using the storage API + * @param {Object} value + * "value" to compare against + */ +async function checkGetImpl(areaName, prop, value) { + let storage = browser.storage[areaName]; + + let data = await storage.get(null); + browser.test.assertEq(value, data[prop], `null getter worked for ${prop} in ${areaName}`); + + data = await storage.get(prop); + browser.test.assertEq(value, data[prop], `string getter worked for ${prop} in ${areaName}`); + + data = await storage.get([prop]); + browser.test.assertEq(value, data[prop], `array getter worked for ${prop} in ${areaName}`); + + data = await storage.get({[prop]: undefined}); + browser.test.assertEq(value, data[prop], `object getter worked for ${prop} in ${areaName}`); +} + +add_task(function* test_local_cache_invalidation() { + function background(checkGet) { + browser.test.onMessage.addListener(async msg => { + if (msg === "set-initial") { + await browser.storage.local.set({"test-prop1": "value1", "test-prop2": "value2"}); + browser.test.sendMessage("set-initial-done"); + } else if (msg === "check") { + await checkGet("local", "test-prop1", "value1"); + await checkGet("local", "test-prop2", "value2"); + browser.test.sendMessage("check-done"); + } + }); + + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["storage"], + }, + background: `(${background})(${checkGetImpl})`, + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + extension.sendMessage("set-initial"); + yield extension.awaitMessage("set-initial-done"); + + Services.obs.notifyObservers(null, "extension-invalidate-storage-cache", ""); + + extension.sendMessage("check"); + yield extension.awaitMessage("check-done"); + + yield extension.unload(); +}); + +add_task(function* test_config_flag_needed() { + function background() { + let promises = []; + let apiTests = [ + {method: "get", args: ["foo"]}, + {method: "set", args: [{foo: "bar"}]}, + {method: "remove", args: ["foo"]}, + {method: "clear", args: []}, + ]; + apiTests.forEach(testDef => { + promises.push(browser.test.assertRejects( + browser.storage.sync[testDef.method](...testDef.args), + "Please set webextensions.storage.sync.enabled to true in about:config", + `storage.sync.${testDef.method} is behind a flag`)); + }); + + Promise.all(promises).then(() => browser.test.notifyPass("flag needed")); + } + + ok(!Preferences.get(STORAGE_SYNC_PREF)); + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["storage"], + }, + background: `(${background})(${checkGetImpl})`, + }); + + yield extension.startup(); + yield extension.awaitFinish("flag needed"); + yield extension.unload(); +}); + +add_task(function* test_reloading_extensions_works() { + // Just some random extension ID that we can re-use + const extensionId = "my-extension-id@1"; + + function loadExtension() { + function background() { + browser.storage.sync.set({"a": "b"}).then(() => { + browser.test.notifyPass("set-works"); + }); + } + + return ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["storage"], + }, + background: `(${background})()`, + }, extensionId); + } + + Preferences.set(STORAGE_SYNC_PREF, true); + + let extension1 = loadExtension(); + + yield extension1.startup(); + yield extension1.awaitFinish("set-works"); + yield extension1.unload(); + + let extension2 = loadExtension(); + + yield extension2.startup(); + yield extension2.awaitFinish("set-works"); + yield extension2.unload(); + + Preferences.reset(STORAGE_SYNC_PREF); +}); + +do_register_cleanup(() => { + Preferences.reset(STORAGE_SYNC_PREF); +}); + +add_task(function* test_backgroundScript() { + async function backgroundScript(checkGet) { + let globalChanges, gResolve; + function clearGlobalChanges() { + globalChanges = new Promise(resolve => { gResolve = resolve; }); + } + clearGlobalChanges(); + let expectedAreaName; + + browser.storage.onChanged.addListener((changes, areaName) => { + browser.test.assertEq(expectedAreaName, areaName, + "Expected area name received by listener"); + gResolve(changes); + }); + + async function checkChanges(areaName, changes, message) { + function checkSub(obj1, obj2) { + for (let prop in obj1) { + browser.test.assertTrue(obj1[prop] !== undefined, + `checkChanges ${areaName} ${prop} is missing (${message})`); + browser.test.assertTrue(obj2[prop] !== undefined, + `checkChanges ${areaName} ${prop} is missing (${message})`); + browser.test.assertEq(obj1[prop].oldValue, obj2[prop].oldValue, + `checkChanges ${areaName} ${prop} old (${message})`); + browser.test.assertEq(obj1[prop].newValue, obj2[prop].newValue, + `checkChanges ${areaName} ${prop} new (${message})`); + } + } + + const recentChanges = await globalChanges; + checkSub(changes, recentChanges); + checkSub(recentChanges, changes); + clearGlobalChanges(); + } + + /* eslint-disable dot-notation */ + async function runTests(areaName) { + expectedAreaName = areaName; + let storage = browser.storage[areaName]; + // Set some data and then test getters. + try { + await storage.set({"test-prop1": "value1", "test-prop2": "value2"}); + await checkChanges(areaName, + {"test-prop1": {newValue: "value1"}, "test-prop2": {newValue: "value2"}}, + "set (a)"); + + await checkGet(areaName, "test-prop1", "value1"); + await checkGet(areaName, "test-prop2", "value2"); + + let data = await storage.get({"test-prop1": undefined, "test-prop2": undefined, "other": "default"}); + browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (a)"); + browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (a)"); + browser.test.assertEq("default", data["other"], "other correct"); + + data = await storage.get(["test-prop1", "test-prop2", "other"]); + browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (b)"); + browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (b)"); + browser.test.assertFalse("other" in data, "other correct"); + + // Remove data in various ways. + await storage.remove("test-prop1"); + await checkChanges(areaName, {"test-prop1": {oldValue: "value1"}}, "remove string"); + + data = await storage.get(["test-prop1", "test-prop2"]); + browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove string)"); + browser.test.assertTrue("test-prop2" in data, "prop2 present (remove string)"); + + await storage.set({"test-prop1": "value1"}); + await checkChanges(areaName, {"test-prop1": {newValue: "value1"}}, "set (c)"); + + data = await storage.get(["test-prop1", "test-prop2"]); + browser.test.assertEq(data["test-prop1"], "value1", "prop1 correct (c)"); + browser.test.assertEq(data["test-prop2"], "value2", "prop2 correct (c)"); + + await storage.remove(["test-prop1", "test-prop2"]); + await checkChanges(areaName, + {"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}}, + "remove array"); + + data = await storage.get(["test-prop1", "test-prop2"]); + browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove array)"); + browser.test.assertFalse("test-prop2" in data, "prop2 absent (remove array)"); + + // test storage.clear + await storage.set({"test-prop1": "value1", "test-prop2": "value2"}); + // Make sure that set() handler happened before we clear the + // promise again. + await globalChanges; + + clearGlobalChanges(); + await storage.clear(); + + await checkChanges(areaName, + {"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}}, + "clear"); + data = await storage.get(["test-prop1", "test-prop2"]); + browser.test.assertFalse("test-prop1" in data, "prop1 absent (clear)"); + browser.test.assertFalse("test-prop2" in data, "prop2 absent (clear)"); + + // Make sure we can store complex JSON data. + // known previous values + await storage.set({"test-prop1": "value1", "test-prop2": "value2"}); + + // Make sure the set() handler landed. + await globalChanges; + + clearGlobalChanges(); + await storage.set({ + "test-prop1": { + str: "hello", + bool: true, + null: null, + undef: undefined, + obj: {}, + arr: [1, 2], + date: new Date(0), + regexp: /regexp/, + func: function func() {}, + window, + }, + }); + + await storage.set({"test-prop2": function func() {}}); + const recentChanges = await globalChanges; + + browser.test.assertEq("value1", recentChanges["test-prop1"].oldValue, "oldValue correct"); + browser.test.assertEq("object", typeof(recentChanges["test-prop1"].newValue), "newValue is obj"); + clearGlobalChanges(); + + data = await storage.get({"test-prop1": undefined, "test-prop2": undefined}); + let obj = data["test-prop1"]; + + browser.test.assertEq("hello", obj.str, "string part correct"); + browser.test.assertEq(true, obj.bool, "bool part correct"); + browser.test.assertEq(null, obj.null, "null part correct"); + browser.test.assertEq(undefined, obj.undef, "undefined part correct"); + browser.test.assertEq(undefined, obj.func, "function part correct"); + browser.test.assertEq(undefined, obj.window, "window part correct"); + browser.test.assertEq("1970-01-01T00:00:00.000Z", obj.date, "date part correct"); + browser.test.assertEq("/regexp/", obj.regexp, "regexp part correct"); + browser.test.assertEq("object", typeof(obj.obj), "object part correct"); + browser.test.assertTrue(Array.isArray(obj.arr), "array part present"); + browser.test.assertEq(1, obj.arr[0], "arr[0] part correct"); + browser.test.assertEq(2, obj.arr[1], "arr[1] part correct"); + browser.test.assertEq(2, obj.arr.length, "arr.length part correct"); + + obj = data["test-prop2"]; + + browser.test.assertEq("[object Object]", {}.toString.call(obj), "function serialized as a plain object"); + browser.test.assertEq(0, Object.keys(obj).length, "function serialized as an empty object"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("storage"); + } + } + + browser.test.onMessage.addListener(msg => { + let promise; + if (msg === "test-local") { + promise = runTests("local"); + } else if (msg === "test-sync") { + promise = runTests("sync"); + } + promise.then(() => browser.test.sendMessage("test-finished")); + }); + + browser.test.sendMessage("ready"); + } + + let extensionData = { + background: `(${backgroundScript})(${checkGetImpl})`, + manifest: { + permissions: ["storage"], + }, + }; + + Preferences.set(STORAGE_SYNC_PREF, true); + + let extension = ExtensionTestUtils.loadExtension(extensionData); + yield extension.startup(); + yield extension.awaitMessage("ready"); + + extension.sendMessage("test-local"); + yield extension.awaitMessage("test-finished"); + + extension.sendMessage("test-sync"); + yield extension.awaitMessage("test-finished"); + + Preferences.reset(STORAGE_SYNC_PREF); + yield extension.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js new file mode 100644 index 000000000..4258289e3 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js @@ -0,0 +1,1073 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +do_get_profile(); // so we can use FxAccounts + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://services-common/utils.js"); +Cu.import("resource://gre/modules/ExtensionStorageSync.jsm"); +const { + CollectionKeyEncryptionRemoteTransformer, + cryptoCollection, + idToKey, + extensionIdToCollectionId, + keyToId, +} = Cu.import("resource://gre/modules/ExtensionStorageSync.jsm"); +Cu.import("resource://services-sync/engines/extension-storage.js"); +Cu.import("resource://services-sync/keys.js"); +Cu.import("resource://services-sync/util.js"); + +/* globals BulkKeyBundle, CommonUtils, EncryptionRemoteTransformer */ +/* globals KeyRingEncryptionRemoteTransformer */ +/* globals Utils */ + +function handleCannedResponse(cannedResponse, request, response) { + response.setStatusLine(null, cannedResponse.status.status, + cannedResponse.status.statusText); + // send the headers + for (let headerLine of cannedResponse.sampleHeaders) { + let headerElements = headerLine.split(":"); + response.setHeader(headerElements[0], headerElements[1].trimLeft()); + } + response.setHeader("Date", (new Date()).toUTCString()); + + response.write(cannedResponse.responseBody); +} + +function collectionRecordsPath(collectionId) { + return `/buckets/default/collections/${collectionId}/records`; +} + +class KintoServer { + constructor() { + // Set up an HTTP Server + this.httpServer = new HttpServer(); + this.httpServer.start(-1); + + // Map<CollectionId, Set<Object>> corresponding to the data in the + // Kinto server + this.collections = new Map(); + + // ETag to serve with responses + this.etag = 1; + + this.port = this.httpServer.identity.primaryPort; + // POST requests we receive from the client go here + this.posts = []; + // DELETEd buckets will go here. + this.deletedBuckets = []; + // Anything in here will force the next POST to generate a conflict + this.conflicts = []; + + this.installConfigPath(); + this.installBatchPath(); + this.installCatchAll(); + } + + clearPosts() { + this.posts = []; + } + + getPosts() { + return this.posts; + } + + getDeletedBuckets() { + return this.deletedBuckets; + } + + installConfigPath() { + const configPath = "/v1/"; + const responseBody = JSON.stringify({ + "settings": {"batch_max_requests": 25}, + "url": `http://localhost:${this.port}/v1/`, + "documentation": "https://kinto.readthedocs.org/", + "version": "1.5.1", + "commit": "cbc6f58", + "hello": "kinto", + }); + const configResponse = { + "sampleHeaders": [ + "Access-Control-Allow-Origin: *", + "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", + "Content-Type: application/json; charset=UTF-8", + "Server: waitress", + ], + "status": {status: 200, statusText: "OK"}, + "responseBody": responseBody, + }; + + function handleGetConfig(request, response) { + if (request.method != "GET") { + dump(`ARGH, got ${request.method}\n`); + } + return handleCannedResponse(configResponse, request, response); + } + + this.httpServer.registerPathHandler(configPath, handleGetConfig); + } + + installBatchPath() { + const batchPath = "/v1/batch"; + + function handlePost(request, response) { + let bodyStr = CommonUtils.readBytesFromInputStream(request.bodyInputStream); + let body = JSON.parse(bodyStr); + let defaults = body.defaults; + for (let req of body.requests) { + let headers = Object.assign({}, defaults && defaults.headers || {}, req.headers); + // FIXME: assert auth is "Bearer ...token..." + this.posts.push(Object.assign({}, req, {headers})); + } + + response.setStatusLine(null, 200, "OK"); + response.setHeader("Content-Type", "application/json; charset=UTF-8"); + response.setHeader("Date", (new Date()).toUTCString()); + + let postResponse = { + responses: body.requests.map(req => { + let oneBody; + if (req.method == "DELETE") { + let id = req.path.match(/^\/buckets\/default\/collections\/.+\/records\/(.+)$/)[1]; + oneBody = { + "data": { + "deleted": true, + "id": id, + "last_modified": this.etag, + }, + }; + } else { + oneBody = {"data": Object.assign({}, req.body.data, {last_modified: this.etag}), + "permissions": []}; + } + + return { + path: req.path, + status: 201, // FIXME -- only for new posts?? + headers: {"ETag": 3000}, // FIXME??? + body: oneBody, + }; + }), + }; + + if (this.conflicts.length > 0) { + const {collectionId, encrypted} = this.conflicts.shift(); + this.collections.get(collectionId).add(encrypted); + dump(`responding with etag ${this.etag}\n`); + postResponse = { + responses: body.requests.map(req => { + return { + path: req.path, + status: 412, + headers: {"ETag": this.etag}, // is this correct?? + body: { + details: { + existing: encrypted, + }, + }, + }; + }), + }; + } + + response.write(JSON.stringify(postResponse)); + + // "sampleHeaders": [ + // "Access-Control-Allow-Origin: *", + // "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", + // "Server: waitress", + // "Etag: \"4000\"" + // ], + } + + this.httpServer.registerPathHandler(batchPath, handlePost.bind(this)); + } + + installCatchAll() { + this.httpServer.registerPathHandler("/", (request, response) => { + dump(`got request: ${request.method}:${request.path}?${request.queryString}\n`); + dump(`${CommonUtils.readBytesFromInputStream(request.bodyInputStream)}\n`); + }); + } + + installCollection(collectionId) { + this.collections.set(collectionId, new Set()); + + const remoteRecordsPath = "/v1" + collectionRecordsPath(encodeURIComponent(collectionId)); + + function handleGetRecords(request, response) { + if (request.method != "GET") { + do_throw(`only GET is supported on ${remoteRecordsPath}`); + } + + response.setStatusLine(null, 200, "OK"); + response.setHeader("Content-Type", "application/json; charset=UTF-8"); + response.setHeader("Date", (new Date()).toUTCString()); + response.setHeader("ETag", this.etag.toString()); + + const records = this.collections.get(collectionId); + // Can't JSON a Set directly, so convert to Array + let data = Array.from(records); + if (request.queryString.includes("_since=")) { + data = data.filter(r => !(r._inPast || false)); + } + + // Remove records that we only needed to serve once. + // FIXME: come up with a more coherent idea of time here. + // See bug 1321570. + for (const record of records) { + if (record._onlyOnce) { + records.delete(record); + } + } + + const body = JSON.stringify({ + "data": data, + }); + response.write(body); + } + + this.httpServer.registerPathHandler(remoteRecordsPath, handleGetRecords.bind(this)); + } + + installDeleteBucket() { + this.httpServer.registerPrefixHandler("/v1/buckets/", (request, response) => { + if (request.method != "DELETE") { + dump(`got a non-delete action on bucket: ${request.method} ${request.path}\n`); + return; + } + + const noPrefix = request.path.slice("/v1/buckets/".length); + const [bucket, afterBucket] = noPrefix.split("/", 1); + if (afterBucket && afterBucket != "") { + dump(`got a delete for a non-bucket: ${request.method} ${request.path}\n`); + } + + this.deletedBuckets.push(bucket); + // Fake like this actually deletes the records. + for (const [, set] of this.collections) { + set.clear(); + } + + response.write(JSON.stringify({ + data: { + deleted: true, + last_modified: 1475161309026, + id: "b09f1618-d789-302d-696e-74ec53ee18a8", // FIXME + }, + })); + }); + } + + // Utility function to install a keyring at the start of a test. + installKeyRing(keysData, etag, {conflict = false} = {}) { + this.installCollection("storage-sync-crypto"); + const keysRecord = { + "id": "keys", + "keys": keysData, + "last_modified": etag, + }; + this.etag = etag; + const methodName = conflict ? "encryptAndAddRecordWithConflict" : "encryptAndAddRecord"; + this[methodName](new KeyRingEncryptionRemoteTransformer(), + "storage-sync-crypto", keysRecord); + } + + // Add an already-encrypted record. + addRecord(collectionId, record) { + this.collections.get(collectionId).add(record); + } + + // Add a record that is only served if no `_since` is present. + // + // Since in real life, Kinto only serves a record as part of a + // changes feed if `_since` is before the record's modification + // time, this can be helpful to test certain kinds of syncing logic. + // + // FIXME: tracking of "time" in this mock server really needs to be + // implemented correctly rather than these hacks. See bug 1321570. + addRecordInPast(collectionId, record) { + record._inPast = true; + this.addRecord(collectionId, record); + } + + encryptAndAddRecord(transformer, collectionId, record) { + return transformer.encode(record).then(encrypted => { + this.addRecord(collectionId, encrypted); + }); + } + + // Like encryptAndAddRecord, but add a flag that will only serve + // this record once. + // + // Since in real life, Kinto only serves a record as part of a changes feed + // once, this can be useful for testing complicated syncing logic. + // + // FIXME: This kind of logic really needs to be subsumed into some + // more-realistic tracking of "time" (simulated by etags). See bug 1321570. + encryptAndAddRecordOnlyOnce(transformer, collectionId, record) { + return transformer.encode(record).then(encrypted => { + encrypted._onlyOnce = true; + this.addRecord(collectionId, encrypted); + }); + } + + // Conflicts block the next push and then appear in the collection specified. + encryptAndAddRecordWithConflict(transformer, collectionId, record) { + return transformer.encode(record).then(encrypted => { + this.conflicts.push({collectionId, encrypted}); + }); + } + + clearCollection(collectionId) { + this.collections.get(collectionId).clear(); + } + + stop() { + this.httpServer.stop(() => { }); + } +} + +// Run a block of code with access to a KintoServer. +function* withServer(f) { + let server = new KintoServer(); + // Point the sync.storage client to use the test server we've just started. + Services.prefs.setCharPref("webextensions.storage.sync.serverURL", + `http://localhost:${server.port}/v1`); + try { + yield* f(server); + } finally { + server.stop(); + } +} + +// Run a block of code with access to both a sync context and a +// KintoServer. This is meant as a workaround for eslint's refusal to +// let me have 5 nested callbacks. +function* withContextAndServer(f) { + yield* withSyncContext(function* (context) { + yield* withServer(function* (server) { + yield* f(context, server); + }); + }); +} + +// Run a block of code with fxa mocked out to return a specific user. +function* withSignedInUser(user, f) { + const oldESSFxAccounts = ExtensionStorageSync._fxaService; + const oldERTFxAccounts = EncryptionRemoteTransformer.prototype._fxaService; + ExtensionStorageSync._fxaService = EncryptionRemoteTransformer.prototype._fxaService = { + getSignedInUser() { + return Promise.resolve(user); + }, + getOAuthToken() { + return Promise.resolve("some-access-token"); + }, + sessionStatus() { + return Promise.resolve(true); + }, + }; + + try { + yield* f(); + } finally { + ExtensionStorageSync._fxaService = oldESSFxAccounts; + EncryptionRemoteTransformer.prototype._fxaService = oldERTFxAccounts; + } +} + +// Some assertions that make it easier to write tests about what was +// posted and when. + +// Assert that the request was made with the correct access token. +// This should be true of all requests, so this is usually called from +// another assertion. +function assertAuthenticatedRequest(post) { + equal(post.headers.Authorization, "Bearer some-access-token"); +} + +// Assert that this post was made with the correct request headers to +// create a new resource while protecting against someone else +// creating it at the same time (in other words, "If-None-Match: *"). +// Also calls assertAuthenticatedRequest(post). +function assertPostedNewRecord(post) { + assertAuthenticatedRequest(post); + equal(post.headers["If-None-Match"], "*"); +} + +// Assert that this post was made with the correct request headers to +// update an existing resource while protecting against concurrent +// modification (in other words, `If-Match: "${etag}"`). +// Also calls assertAuthenticatedRequest(post). +function assertPostedUpdatedRecord(post, since) { + assertAuthenticatedRequest(post); + equal(post.headers["If-Match"], `"${since}"`); +} + +// Assert that this post was an encrypted keyring, and produce the +// decrypted body. Sanity check the body while we're here. +const assertPostedEncryptedKeys = Task.async(function* (post) { + equal(post.path, collectionRecordsPath("storage-sync-crypto") + "/keys"); + + let body = yield new KeyRingEncryptionRemoteTransformer().decode(post.body.data); + ok(body.keys, `keys object should be present in decoded body`); + ok(body.keys.default, `keys object should have a default key`); + return body; +}); + +// assertEqual, but for keyring[extensionId] == key. +function assertKeyRingKey(keyRing, extensionId, expectedKey, message) { + if (!message) { + message = `expected keyring's key for ${extensionId} to match ${expectedKey.keyPairB64}`; + } + ok(keyRing.hasKeysFor([extensionId]), + `expected keyring to have a key for ${extensionId}\n`); + deepEqual(keyRing.keyForCollection(extensionId).keyPairB64, expectedKey.keyPairB64, + message); +} + +// Tests using this ID will share keys in local storage, so be careful. +const defaultExtensionId = "{13bdde76-4dc7-11e6-9bdc-54ee758d6342}"; +const defaultExtension = {id: defaultExtensionId}; + +const BORING_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; +const ANOTHER_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde0"; +const loggedInUser = { + uid: "0123456789abcdef0123456789abcdef", + kB: BORING_KB, + oauthTokens: { + "sync:addon-storage": { + token: "some-access-token", + }, + }, +}; +const defaultCollectionId = extensionIdToCollectionId(loggedInUser, defaultExtensionId); + +function uuid() { + const uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); + return uuidgen.generateUUID().toString(); +} + +add_task(function* test_key_to_id() { + equal(keyToId("foo"), "key-foo"); + equal(keyToId("my-new-key"), "key-my_2D_new_2D_key"); + equal(keyToId(""), "key-"); + equal(keyToId("â„¢"), "key-_2122_"); + equal(keyToId("\b"), "key-_8_"); + equal(keyToId("abc\ndef"), "key-abc_A_def"); + equal(keyToId("Kinto's fancy_string"), "key-Kinto_27_s_20_fancy_5F_string"); + + const KEYS = ["foo", "my-new-key", "", "Kinto's fancy_string", "â„¢", "\b"]; + for (let key of KEYS) { + equal(idToKey(keyToId(key)), key); + } + + equal(idToKey("hi"), null); + equal(idToKey("-key-hi"), null); + equal(idToKey("key--abcd"), null); + equal(idToKey("key-%"), null); + equal(idToKey("key-_HI"), null); + equal(idToKey("key-_HI_"), null); + equal(idToKey("key-"), ""); + equal(idToKey("key-1"), "1"); + equal(idToKey("key-_2D_"), "-"); +}); + +add_task(function* test_extension_id_to_collection_id() { + const newKBUser = Object.assign(loggedInUser, {kB: ANOTHER_KB}); + const extensionId = "{9419cce6-5435-11e6-84bf-54ee758d6342}"; + const extensionId2 = "{9419cce6-5435-11e6-84bf-54ee758d6343}"; + + // "random" 32-char hex userid + equal(extensionIdToCollectionId(loggedInUser, extensionId), + "abf4e257dad0c89027f8f25bd196d4d69c100df375655a0c49f4cea7b791ea7d"); + equal(extensionIdToCollectionId(loggedInUser, extensionId), + extensionIdToCollectionId(newKBUser, extensionId)); + equal(extensionIdToCollectionId(loggedInUser, extensionId2), + "6584b0153336fb274912b31a3225c15a92b703cdc3adfe1917c1aa43122a52b8"); +}); + +add_task(function* ensureKeysFor_posts_new_keys() { + const extensionId = uuid(); + yield* withContextAndServer(function* (context, server) { + yield* withSignedInUser(loggedInUser, function* () { + server.installCollection("storage-sync-crypto"); + server.etag = 1000; + + let newKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]); + ok(newKeys.hasKeysFor([extensionId]), `key isn't present for ${extensionId}`); + + let posts = server.getPosts(); + equal(posts.length, 1); + const post = posts[0]; + assertPostedNewRecord(post); + const body = yield assertPostedEncryptedKeys(post); + ok(body.keys.collections[extensionId], `keys object should have a key for ${extensionId}`); + + // Try adding another key to make sure that the first post was + // OK, even on a new profile. + yield cryptoCollection._clear(); + server.clearPosts(); + // Restore the first posted keyring + server.addRecordInPast("storage-sync-crypto", post.body.data); + const extensionId2 = uuid(); + newKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId2]); + ok(newKeys.hasKeysFor([extensionId]), `didn't forget key for ${extensionId}`); + ok(newKeys.hasKeysFor([extensionId2]), `new key generated for ${extensionId2}`); + + posts = server.getPosts(); + // FIXME: some kind of bug where we try to repush the + // server_wins version multiple times in a single sync. We + // actually push 5 times as of this writing. + // See bug 1321571. + // equal(posts.length, 1); + const newPost = posts[posts.length - 1]; + const newBody = yield assertPostedEncryptedKeys(newPost); + ok(newBody.keys.collections[extensionId], `keys object should have a key for ${extensionId}`); + ok(newBody.keys.collections[extensionId2], `keys object should have a key for ${extensionId2}`); + + }); + }); +}); + +add_task(function* ensureKeysFor_pulls_key() { + // ensureKeysFor is implemented by adding a key to our local record + // and doing a sync. This means that if the same key exists + // remotely, we get a "conflict". Ensure that we handle this + // correctly -- we keep the server key (since presumably it's + // already been used to encrypt records) and we don't wipe out other + // collections' keys. + const extensionId = uuid(); + const extensionId2 = uuid(); + const DEFAULT_KEY = new BulkKeyBundle("[default]"); + DEFAULT_KEY.generateRandom(); + const RANDOM_KEY = new BulkKeyBundle(extensionId); + RANDOM_KEY.generateRandom(); + yield* withContextAndServer(function* (context, server) { + yield* withSignedInUser(loggedInUser, function* () { + const keysData = { + "default": DEFAULT_KEY.keyPairB64, + "collections": { + [extensionId]: RANDOM_KEY.keyPairB64, + }, + }; + server.installKeyRing(keysData, 999); + + let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]); + assertKeyRingKey(collectionKeys, extensionId, RANDOM_KEY); + + let posts = server.getPosts(); + equal(posts.length, 0, + "ensureKeysFor shouldn't push when the server keyring has the right key"); + + // Another client generates a key for extensionId2 + const newKey = new BulkKeyBundle(extensionId2); + newKey.generateRandom(); + keysData.collections[extensionId2] = newKey.keyPairB64; + server.clearCollection("storage-sync-crypto"); + server.installKeyRing(keysData, 1000); + + let newCollectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId, extensionId2]); + assertKeyRingKey(newCollectionKeys, extensionId2, newKey); + assertKeyRingKey(newCollectionKeys, extensionId, RANDOM_KEY, + `ensureKeysFor shouldn't lose the old key for ${extensionId}`); + + posts = server.getPosts(); + equal(posts.length, 0, "ensureKeysFor shouldn't push when updating keys"); + }); + }); +}); + +add_task(function* ensureKeysFor_handles_conflicts() { + // Syncing is done through a pull followed by a push of any merged + // changes. Accordingly, the only way to have a "true" conflict -- + // i.e. with the server rejecting a change -- is if + // someone pushes changes between our pull and our push. Ensure that + // if this happens, we still behave sensibly (keep the remote key). + const extensionId = uuid(); + const DEFAULT_KEY = new BulkKeyBundle("[default]"); + DEFAULT_KEY.generateRandom(); + const RANDOM_KEY = new BulkKeyBundle(extensionId); + RANDOM_KEY.generateRandom(); + yield* withContextAndServer(function* (context, server) { + yield* withSignedInUser(loggedInUser, function* () { + const keysData = { + "default": DEFAULT_KEY.keyPairB64, + "collections": { + [extensionId]: RANDOM_KEY.keyPairB64, + }, + }; + server.installKeyRing(keysData, 765, {conflict: true}); + + yield cryptoCollection._clear(); + + let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]); + assertKeyRingKey(collectionKeys, extensionId, RANDOM_KEY, + `syncing keyring should keep the server key for ${extensionId}`); + + let posts = server.getPosts(); + equal(posts.length, 1, + "syncing keyring should have tried to post a keyring"); + const failedPost = posts[0]; + assertPostedNewRecord(failedPost); + let body = yield assertPostedEncryptedKeys(failedPost); + // This key will be the one the client generated locally, so + // we don't know what its value will be + ok(body.keys.collections[extensionId], + `decrypted failed post should have a key for ${extensionId}`); + notEqual(body.keys.collections[extensionId], RANDOM_KEY.keyPairB64, + `decrypted failed post should have a randomly-generated key for ${extensionId}`); + }); + }); +}); + +add_task(function* checkSyncKeyRing_reuploads_keys() { + // Verify that when keys are present, they are reuploaded with the + // new kB when we call touchKeys(). + const extensionId = uuid(); + let extensionKey; + yield* withContextAndServer(function* (context, server) { + yield* withSignedInUser(loggedInUser, function* () { + server.installCollection("storage-sync-crypto"); + server.etag = 765; + + yield cryptoCollection._clear(); + + // Do an `ensureKeysFor` to generate some keys. + let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]); + ok(collectionKeys.hasKeysFor([extensionId]), + `ensureKeysFor should return a keyring that has a key for ${extensionId}`); + extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64; + equal(server.getPosts().length, 1, + "generating a key that doesn't exist on the server should post it"); + }); + + // The user changes their password. This is their new kB, with + // the last f changed to an e. + const NOVEL_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdee"; + const newUser = Object.assign({}, loggedInUser, {kB: NOVEL_KB}); + let postedKeys; + yield* withSignedInUser(newUser, function* () { + yield ExtensionStorageSync.checkSyncKeyRing(); + + let posts = server.getPosts(); + equal(posts.length, 2, + "when kB changes, checkSyncKeyRing should post the keyring reencrypted with the new kB"); + postedKeys = posts[1]; + assertPostedUpdatedRecord(postedKeys, 765); + + let body = yield assertPostedEncryptedKeys(postedKeys); + deepEqual(body.keys.collections[extensionId], extensionKey, + `the posted keyring should have the same key for ${extensionId} as the old one`); + }); + + // Verify that with the old kB, we can't decrypt the record. + yield* withSignedInUser(loggedInUser, function* () { + let error; + try { + yield new KeyRingEncryptionRemoteTransformer().decode(postedKeys.body.data); + } catch (e) { + error = e; + } + ok(error, "decrypting the keyring with the old kB should fail"); + ok(Utils.isHMACMismatch(error) || KeyRingEncryptionRemoteTransformer.isOutdatedKB(error), + "decrypting the keyring with the old kB should throw an HMAC mismatch"); + }); + }); +}); + +add_task(function* checkSyncKeyRing_overwrites_on_conflict() { + // If there is already a record on the server that was encrypted + // with a different kB, we wipe the server, clear sync state, and + // overwrite it with our keys. + const extensionId = uuid(); + const transformer = new KeyRingEncryptionRemoteTransformer(); + let extensionKey; + yield* withSyncContext(function* (context) { + yield* withServer(function* (server) { + // The old device has this kB, which is very similar to the + // current kB but with the last f changed to an e. + const NOVEL_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdee"; + const oldUser = Object.assign({}, loggedInUser, {kB: NOVEL_KB}); + server.installCollection("storage-sync-crypto"); + server.installDeleteBucket(); + server.etag = 765; + yield* withSignedInUser(oldUser, function* () { + const FAKE_KEYRING = { + id: "keys", + keys: {}, + uuid: "abcd", + kbHash: "abcd", + }; + yield server.encryptAndAddRecord(transformer, "storage-sync-crypto", FAKE_KEYRING); + }); + + // Now we have this new user with a different kB. + yield* withSignedInUser(loggedInUser, function* () { + yield cryptoCollection._clear(); + + // Do an `ensureKeysFor` to generate some keys. + // This will try to sync, notice that the record is + // undecryptable, and clear the server. + let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]); + ok(collectionKeys.hasKeysFor([extensionId]), + `ensureKeysFor should always return a keyring with a key for ${extensionId}`); + extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64; + + deepEqual(server.getDeletedBuckets(), ["default"], + "Kinto server should have been wiped when keyring was thrown away"); + + let posts = server.getPosts(); + equal(posts.length, 1, + "new keyring should have been uploaded"); + const postedKeys = posts[0]; + // The POST was to an empty server, so etag shouldn't be respected + equal(postedKeys.headers.Authorization, "Bearer some-access-token", + "keyring upload should be authorized"); + equal(postedKeys.headers["If-None-Match"], "*", + "keyring upload should be to empty Kinto server"); + equal(postedKeys.path, collectionRecordsPath("storage-sync-crypto") + "/keys", + "keyring upload should be to keyring path"); + + let body = yield new KeyRingEncryptionRemoteTransformer().decode(postedKeys.body.data); + ok(body.uuid, "new keyring should have a UUID"); + equal(typeof body.uuid, "string", "keyring UUIDs should be strings"); + notEqual(body.uuid, "abcd", + "new keyring should not have the same UUID as previous keyring"); + ok(body.keys, + "new keyring should have a keys attribute"); + ok(body.keys.default, "new keyring should have a default key"); + // We should keep the extension key that was in our uploaded version. + deepEqual(extensionKey, body.keys.collections[extensionId], + "ensureKeysFor should have returned keyring with the same key that was uploaded"); + + // This should be a no-op; the keys were uploaded as part of ensurekeysfor + yield ExtensionStorageSync.checkSyncKeyRing(); + equal(server.getPosts().length, 1, + "checkSyncKeyRing should not need to post keys after they were reuploaded"); + }); + }); + }); +}); + +add_task(function* checkSyncKeyRing_flushes_on_uuid_change() { + // If we can decrypt the record, but the UUID has changed, that + // means another client has wiped the server and reuploaded a + // keyring, so reset sync state and reupload everything. + const extensionId = uuid(); + const extension = {id: extensionId}; + const collectionId = extensionIdToCollectionId(loggedInUser, extensionId); + const transformer = new KeyRingEncryptionRemoteTransformer(); + yield* withSyncContext(function* (context) { + yield* withServer(function* (server) { + server.installCollection("storage-sync-crypto"); + server.installCollection(collectionId); + server.installDeleteBucket(); + yield* withSignedInUser(loggedInUser, function* () { + yield cryptoCollection._clear(); + + // Do an `ensureKeysFor` to get access to keys. + let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]); + ok(collectionKeys.hasKeysFor([extensionId]), + `ensureKeysFor should always return a keyring that has a key for ${extensionId}`); + const extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64; + + // Set something to make sure that it gets re-uploaded when + // uuid changes. + yield ExtensionStorageSync.set(extension, {"my-key": 5}, context); + yield ExtensionStorageSync.syncAll(); + + let posts = server.getPosts(); + equal(posts.length, 2, + "should have posted a new keyring and an extension datum"); + const postedKeys = posts[0]; + equal(postedKeys.path, collectionRecordsPath("storage-sync-crypto") + "/keys", + "should have posted keyring to /keys"); + + let body = yield transformer.decode(postedKeys.body.data); + ok(body.uuid, + "keyring should have a UUID"); + ok(body.keys, + "keyring should have a keys attribute"); + ok(body.keys.default, + "keyring should have a default key"); + deepEqual(extensionKey, body.keys.collections[extensionId], + "new keyring should have the same key that we uploaded"); + + // Another client comes along and replaces the UUID. + // In real life, this would mean changing the keys too, but + // this test verifies that just changing the UUID is enough. + const newKeyRingData = Object.assign({}, body, { + uuid: "abcd", + // Technically, last_modified should be served outside the + // object, but the transformer will pass it through in + // either direction, so this is OK. + last_modified: 765, + }); + server.clearCollection("storage-sync-crypto"); + server.etag = 765; + yield server.encryptAndAddRecordOnlyOnce(transformer, "storage-sync-crypto", newKeyRingData); + + // Fake adding another extension just so that the keyring will + // really get synced. + const newExtension = uuid(); + const newKeyRing = yield ExtensionStorageSync.ensureKeysFor([newExtension]); + + // This should have detected the UUID change and flushed everything. + // The keyring should, however, be the same, since we just + // changed the UUID of the previously POSTed one. + deepEqual(newKeyRing.keyForCollection(extensionId).keyPairB64, extensionKey, + "ensureKeysFor should have pulled down a new keyring with the same keys"); + + // Syncing should reupload the data for the extension. + yield ExtensionStorageSync.syncAll(); + posts = server.getPosts(); + equal(posts.length, 4, + "should have posted keyring for new extension and reuploaded extension data"); + + const finalKeyRingPost = posts[2]; + const reuploadedPost = posts[3]; + + equal(finalKeyRingPost.path, collectionRecordsPath("storage-sync-crypto") + "/keys", + "keyring for new extension should have been posted to /keys"); + let finalKeyRing = yield transformer.decode(finalKeyRingPost.body.data); + equal(finalKeyRing.uuid, "abcd", + "newly uploaded keyring should preserve UUID from replacement keyring"); + + // Confirm that the data got reuploaded + equal(reuploadedPost.path, collectionRecordsPath(collectionId) + "/key-my_2D_key", + "extension data should be posted to path corresponding to its key"); + let reuploadedData = yield new CollectionKeyEncryptionRemoteTransformer(extensionId).decode(reuploadedPost.body.data); + equal(reuploadedData.key, "my-key", + "extension data should have a key attribute corresponding to the extension data key"); + equal(reuploadedData.data, 5, + "extension data should have a data attribute corresponding to the extension data value"); + }); + }); + }); +}); + +add_task(function* test_storage_sync_pulls_changes() { + const extensionId = defaultExtensionId; + const collectionId = defaultCollectionId; + const extension = defaultExtension; + yield* withContextAndServer(function* (context, server) { + yield* withSignedInUser(loggedInUser, function* () { + let transformer = new CollectionKeyEncryptionRemoteTransformer(extensionId); + server.installCollection(collectionId); + server.installCollection("storage-sync-crypto"); + + let calls = []; + yield ExtensionStorageSync.addOnChangedListener(extension, function() { + calls.push(arguments); + }, context); + + yield ExtensionStorageSync.ensureKeysFor([extensionId]); + yield server.encryptAndAddRecord(transformer, collectionId, { + "id": "key-remote_2D_key", + "key": "remote-key", + "data": 6, + }); + + yield ExtensionStorageSync.syncAll(); + const remoteValue = (yield ExtensionStorageSync.get(extension, "remote-key", context))["remote-key"]; + equal(remoteValue, 6, + "ExtensionStorageSync.get() returns value retrieved from sync"); + + equal(calls.length, 1, + "syncing calls on-changed listener"); + deepEqual(calls[0][0], {"remote-key": {newValue: 6}}); + calls = []; + + // Syncing again doesn't do anything + yield ExtensionStorageSync.syncAll(); + + equal(calls.length, 0, + "syncing again shouldn't call on-changed listener"); + + // Updating the server causes us to pull down the new value + server.etag = 1000; + server.clearCollection(collectionId); + yield server.encryptAndAddRecord(transformer, collectionId, { + "id": "key-remote_2D_key", + "key": "remote-key", + "data": 7, + }); + + yield ExtensionStorageSync.syncAll(); + const remoteValue2 = (yield ExtensionStorageSync.get(extension, "remote-key", context))["remote-key"]; + equal(remoteValue2, 7, + "ExtensionStorageSync.get() returns value updated from sync"); + + equal(calls.length, 1, + "syncing calls on-changed listener on update"); + deepEqual(calls[0][0], {"remote-key": {oldValue: 6, newValue: 7}}); + }); + }); +}); + +add_task(function* test_storage_sync_pushes_changes() { + const extensionId = defaultExtensionId; + const collectionId = defaultCollectionId; + const extension = defaultExtension; + yield* withContextAndServer(function* (context, server) { + yield* withSignedInUser(loggedInUser, function* () { + let transformer = new CollectionKeyEncryptionRemoteTransformer(extensionId); + server.installCollection(collectionId); + server.installCollection("storage-sync-crypto"); + server.etag = 1000; + + yield ExtensionStorageSync.set(extension, {"my-key": 5}, context); + + // install this AFTER we set the key to 5... + let calls = []; + ExtensionStorageSync.addOnChangedListener(extension, function() { + calls.push(arguments); + }, context); + + yield ExtensionStorageSync.syncAll(); + const localValue = (yield ExtensionStorageSync.get(extension, "my-key", context))["my-key"]; + equal(localValue, 5, + "pushing an ExtensionStorageSync value shouldn't change local value"); + + let posts = server.getPosts(); + equal(posts.length, 1, + "pushing a value should cause a post to the server"); + const post = posts[0]; + assertPostedNewRecord(post); + equal(post.path, collectionRecordsPath(collectionId) + "/key-my_2D_key", + "pushing a value should have a path corresponding to its id"); + + const encrypted = post.body.data; + ok(encrypted.ciphertext, + "pushing a value should post an encrypted record"); + ok(!encrypted.data, + "pushing a value should not have any plaintext data"); + equal(encrypted.id, "key-my_2D_key", + "pushing a value should use a kinto-friendly record ID"); + + const record = yield transformer.decode(encrypted); + equal(record.key, "my-key", + "when decrypted, a pushed value should have a key field corresponding to its storage.sync key"); + equal(record.data, 5, + "when decrypted, a pushed value should have a data field corresponding to its storage.sync value"); + equal(record.id, "key-my_2D_key", + "when decrypted, a pushed value should have an id field corresponding to its record ID"); + + equal(calls.length, 0, + "pushing a value shouldn't call the on-changed listener"); + + yield ExtensionStorageSync.set(extension, {"my-key": 6}, context); + yield ExtensionStorageSync.syncAll(); + + // Doesn't push keys because keys were pushed by a previous test. + posts = server.getPosts(); + equal(posts.length, 2, + "updating a value should trigger another push"); + const updatePost = posts[1]; + assertPostedUpdatedRecord(updatePost, 1000); + equal(updatePost.path, collectionRecordsPath(collectionId) + "/key-my_2D_key", + "pushing an updated value should go to the same path"); + + const updateEncrypted = updatePost.body.data; + ok(updateEncrypted.ciphertext, + "pushing an updated value should still be encrypted"); + ok(!updateEncrypted.data, + "pushing an updated value should not have any plaintext visible"); + equal(updateEncrypted.id, "key-my_2D_key", + "pushing an updated value should maintain the same ID"); + }); + }); +}); + +add_task(function* test_storage_sync_pulls_deletes() { + const collectionId = defaultCollectionId; + const extension = defaultExtension; + yield* withContextAndServer(function* (context, server) { + yield* withSignedInUser(loggedInUser, function* () { + server.installCollection(collectionId); + server.installCollection("storage-sync-crypto"); + + yield ExtensionStorageSync.set(extension, {"my-key": 5}, context); + yield ExtensionStorageSync.syncAll(); + server.clearPosts(); + + let calls = []; + yield ExtensionStorageSync.addOnChangedListener(extension, function() { + calls.push(arguments); + }, context); + + yield server.addRecord(collectionId, { + "id": "key-my_2D_key", + "deleted": true, + }); + + yield ExtensionStorageSync.syncAll(); + const remoteValues = (yield ExtensionStorageSync.get(extension, "my-key", context)); + ok(!remoteValues["my-key"], + "ExtensionStorageSync.get() shows value was deleted by sync"); + + equal(server.getPosts().length, 0, + "pulling the delete shouldn't cause posts"); + + equal(calls.length, 1, + "syncing calls on-changed listener"); + deepEqual(calls[0][0], {"my-key": {oldValue: 5}}); + calls = []; + + // Syncing again doesn't do anything + yield ExtensionStorageSync.syncAll(); + + equal(calls.length, 0, + "syncing again shouldn't call on-changed listener"); + }); + }); +}); + +add_task(function* test_storage_sync_pushes_deletes() { + const extensionId = uuid(); + const collectionId = extensionIdToCollectionId(loggedInUser, extensionId); + const extension = {id: extensionId}; + yield cryptoCollection._clear(); + yield* withContextAndServer(function* (context, server) { + yield* withSignedInUser(loggedInUser, function* () { + server.installCollection(collectionId); + server.installCollection("storage-sync-crypto"); + server.etag = 1000; + + yield ExtensionStorageSync.set(extension, {"my-key": 5}, context); + + let calls = []; + ExtensionStorageSync.addOnChangedListener(extension, function() { + calls.push(arguments); + }, context); + + yield ExtensionStorageSync.syncAll(); + let posts = server.getPosts(); + equal(posts.length, 2, + "pushing a non-deleted value should post keys and post the value to the server"); + + yield ExtensionStorageSync.remove(extension, ["my-key"], context); + equal(calls.length, 1, + "deleting a value should call the on-changed listener"); + + yield ExtensionStorageSync.syncAll(); + equal(calls.length, 1, + "pushing a deleted value shouldn't call the on-changed listener"); + + // Doesn't push keys because keys were pushed by a previous test. + posts = server.getPosts(); + equal(posts.length, 3, + "deleting a value should trigger another push"); + const post = posts[2]; + assertPostedUpdatedRecord(post, 1000); + equal(post.path, collectionRecordsPath(collectionId) + "/key-my_2D_key", + "pushing a deleted value should go to the same path"); + ok(post.method, "DELETE"); + ok(!post.body, + "deleting a value shouldn't have a body"); + }); + }); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_topSites.js b/toolkit/components/extensions/test/xpcshell/test_ext_topSites.js new file mode 100644 index 000000000..eb3f552ed --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_topSites.js @@ -0,0 +1,85 @@ +"use strict"; + +Cu.import("resource://gre/modules/NewTabUtils.jsm"); + + +function TestProvider(getLinksFn) { + this.getLinks = getLinksFn; + this._observers = new Set(); +} + +TestProvider.prototype = { + addObserver: function(observer) { + this._observers.add(observer); + }, + notifyLinkChanged: function(link, index = -1, deleted = false) { + this._notifyObservers("onLinkChanged", link, index, deleted); + }, + notifyManyLinksChanged: function() { + this._notifyObservers("onManyLinksChanged"); + }, + _notifyObservers: function(observerMethodName, ...args) { + args.unshift(this); + for (let obs of this._observers) { + if (obs[observerMethodName]) { + obs[observerMethodName].apply(NewTabUtils.links, args); + } + } + }, +}; + +function makeLinks(links) { + // Important: To avoid test failures due to clock jitter on Windows XP, call + // Date.now() once here, not each time through the loop. + let frecency = 0; + let now = Date.now() * 1000; + let places = []; + links.map((link, i) => { + places.push({ + url: link.url, + title: link.title, + lastVisitDate: now - i, + frecency: frecency++, + }); + }); + return places; +} + +add_task(function* test_topSites() { + let expect = [{url: "http://example.com/", title: "site#-1"}, + {url: "http://example0.com/", title: "site#0"}, + {url: "http://example1.com/", title: "site#1"}, + {url: "http://example2.com/", title: "site#2"}, + {url: "http://example3.com/", title: "site#3"}]; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": [ + "topSites", + ], + }, + background() { + browser.topSites.get(result => { + browser.test.sendMessage("done", result); + }); + }, + }); + + + let expectedLinks = makeLinks(expect); + let provider = new TestProvider(done => done(expectedLinks)); + + NewTabUtils.initWithoutProviders(); + NewTabUtils.links.addProvider(provider); + + yield NewTabUtils.links.populateCache(); + + yield extension.startup(); + + let result = yield extension.awaitMessage("done"); + Assert.deepEqual(expect, result, "got topSites"); + + yield extension.unload(); + + NewTabUtils.links.removeProvider(provider); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_getAPILevelForWindow.js b/toolkit/components/extensions/test/xpcshell/test_getAPILevelForWindow.js new file mode 100644 index 000000000..68741a6cc --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_getAPILevelForWindow.js @@ -0,0 +1,55 @@ +"use strict"; + +Cu.import("resource://gre/modules/ExtensionManagement.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +function createWindowWithAddonId(addonId) { + let baseURI = Services.io.newURI("about:blank", null, null); + let originAttributes = {addonId}; + let principal = Services.scriptSecurityManager + .createCodebasePrincipal(baseURI, originAttributes); + let chromeNav = Services.appShell.createWindowlessBrowser(true); + let interfaceRequestor = chromeNav.QueryInterface(Ci.nsIInterfaceRequestor); + let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell); + docShell.createAboutBlankContentViewer(principal); + + return {chromeNav, window: docShell.contentViewer.DOMDocument.defaultView}; +} + +add_task(function* test_eventpages() { + const {getAPILevelForWindow, getAddonIdForWindow} = ExtensionManagement; + const {NO_PRIVILEGES, FULL_PRIVILEGES} = ExtensionManagement.API_LEVELS; + const FAKE_ADDON_ID = "fakeAddonId"; + const OTHER_ADDON_ID = "otherFakeAddonId"; + const EMPTY_ADDON_ID = ""; + + let fakeAddonId = createWindowWithAddonId(FAKE_ADDON_ID); + equal(getAddonIdForWindow(fakeAddonId.window), FAKE_ADDON_ID, + "the window has the expected addonId"); + + let apiLevel = getAPILevelForWindow(fakeAddonId.window, FAKE_ADDON_ID); + equal(apiLevel, FULL_PRIVILEGES, + "apiLevel for the window with the right addonId should be FULL_PRIVILEGES"); + + apiLevel = getAPILevelForWindow(fakeAddonId.window, OTHER_ADDON_ID); + equal(apiLevel, NO_PRIVILEGES, + "apiLevel for the window with a different addonId should be NO_PRIVILEGES"); + + fakeAddonId.chromeNav.close(); + + // NOTE: check that window with an empty addon Id (which are window that are + // not Extensions pages) always get no WebExtensions APIs. + let emptyAddonId = createWindowWithAddonId(EMPTY_ADDON_ID); + equal(getAddonIdForWindow(emptyAddonId.window), EMPTY_ADDON_ID, + "the window has the expected addonId"); + + apiLevel = getAPILevelForWindow(emptyAddonId.window, EMPTY_ADDON_ID); + equal(apiLevel, NO_PRIVILEGES, + "apiLevel for empty addonId should be NO_PRIVILEGES"); + + apiLevel = getAPILevelForWindow(emptyAddonId.window, OTHER_ADDON_ID); + equal(apiLevel, NO_PRIVILEGES, + "apiLevel for an 'empty addonId' window should be always NO_PRIVILEGES"); + + emptyAddonId.chromeNav.close(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_locale_converter.js b/toolkit/components/extensions/test/xpcshell/test_locale_converter.js new file mode 100644 index 000000000..c8b1ee92b --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_locale_converter.js @@ -0,0 +1,133 @@ +"use strict"; + +const convService = Cc["@mozilla.org/streamConverters;1"] + .getService(Ci.nsIStreamConverterService); + +const UUID = "72b61ee3-aceb-476c-be1b-0822b036c9f1"; +const ADDON_ID = "test@web.extension"; +const URI = NetUtil.newURI(`moz-extension://${UUID}/file.css`); + +const FROM_TYPE = "application/vnd.mozilla.webext.unlocalized"; +const TO_TYPE = "text/css"; + + +function StringStream(string) { + let stream = Cc["@mozilla.org/io/string-input-stream;1"] + .createInstance(Ci.nsIStringInputStream); + + stream.data = string; + return stream; +} + + +// Initialize the policy service with a stub localizer for our +// add-on ID. +add_task(function* init() { + const aps = Cc["@mozilla.org/addons/policy-service;1"] + .getService(Ci.nsIAddonPolicyService).wrappedJSObject; + + let oldCallback = aps.setExtensionURIToAddonIdCallback(uri => { + if (uri.host == UUID) { + return ADDON_ID; + } + }); + + aps.setAddonLocalizeCallback(ADDON_ID, string => { + return string.replace(/__MSG_(.*?)__/g, "<localized-$1>"); + }); + + do_register_cleanup(() => { + aps.setExtensionURIToAddonIdCallback(oldCallback); + aps.setAddonLocalizeCallback(ADDON_ID, null); + }); +}); + + +// Test that the synchronous converter works as expected with a +// simple string. +add_task(function* testSynchronousConvert() { + let stream = StringStream("Foo __MSG_xxx__ bar __MSG_yyy__ baz"); + + let resultStream = convService.convert(stream, FROM_TYPE, TO_TYPE, URI); + + let result = NetUtil.readInputStreamToString(resultStream, resultStream.available()); + + equal(result, "Foo <localized-xxx> bar <localized-yyy> baz"); +}); + + +// Test that the asynchronous converter works as expected with input +// split into multiple chunks, and a boundary in the middle of a +// replacement token. +add_task(function* testAsyncConvert() { + let listener; + let awaitResult = new Promise((resolve, reject) => { + listener = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener]), + + onDataAvailable(request, context, inputStream, offset, count) { + this.resultParts.push(NetUtil.readInputStreamToString(inputStream, count)); + }, + + onStartRequest() { + ok(!("resultParts" in this)); + this.resultParts = []; + }, + + onStopRequest(request, context, statusCode) { + if (!Components.isSuccessCode(statusCode)) { + reject(new Error(statusCode)); + } + + resolve(this.resultParts.join("\n")); + }, + }; + }); + + let parts = ["Foo __MSG_x", "xx__ bar __MSG_yyy__ baz"]; + + let converter = convService.asyncConvertData(FROM_TYPE, TO_TYPE, listener, URI); + converter.onStartRequest(null, null); + + for (let part of parts) { + converter.onDataAvailable(null, null, StringStream(part), 0, part.length); + } + + converter.onStopRequest(null, null, Cr.NS_OK); + + + let result = yield awaitResult; + equal(result, "Foo <localized-xxx> bar <localized-yyy> baz"); +}); + + +// Test that attempting to initialize a converter with the URI of a +// nonexistent WebExtension fails. +add_task(function* testInvalidUUID() { + let uri = NetUtil.newURI("moz-extension://eb4f3be8-41c9-4970-aa6d-b84d1ecc02b2/file.css"); + let stream = StringStream("Foo __MSG_xxx__ bar __MSG_yyy__ baz"); + + // Assert.throws raise a TypeError exception when the expected param + // is an arrow function. (See Bug 1237961 for rationale) + let expectInvalidContextException = function(e) { + return e.result === Cr.NS_ERROR_INVALID_ARG && /Invalid context/.test(e); + }; + + Assert.throws(() => { + convService.convert(stream, FROM_TYPE, TO_TYPE, uri); + }, expectInvalidContextException); + + Assert.throws(() => { + let listener = {QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener])}; + + convService.asyncConvertData(FROM_TYPE, TO_TYPE, listener, uri); + }, expectInvalidContextException); +}); + + +// Test that an empty stream does not throw an NS_ERROR_ILLEGAL_VALUE. +add_task(function* testEmptyStream() { + let stream = StringStream(""); + let resultStream = convService.convert(stream, FROM_TYPE, TO_TYPE, URI); + equal(resultStream.data, ""); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_locale_data.js b/toolkit/components/extensions/test/xpcshell/test_locale_data.js new file mode 100644 index 000000000..c3cd44e57 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_locale_data.js @@ -0,0 +1,130 @@ +"use strict"; + +Cu.import("resource://gre/modules/Extension.jsm"); + +/* globals ExtensionData */ + +const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); + +function* generateAddon(data) { + let id = uuidGenerator.generateUUID().number; + + data = Object.assign({embedded: true}, data); + data.manifest = Object.assign({applications: {gecko: {id}}}, data.manifest); + + let xpi = Extension.generateXPI(data); + do_register_cleanup(() => { + Services.obs.notifyObservers(xpi, "flush-cache-entry", null); + xpi.remove(false); + }); + + let fileURI = Services.io.newFileURI(xpi); + let jarURI = NetUtil.newURI(`jar:${fileURI.spec}!/webextension/`); + + let extension = new ExtensionData(jarURI); + yield extension.readManifest(); + + return extension; +} + +add_task(function* testMissingDefaultLocale() { + let extension = yield generateAddon({ + "files": { + "_locales/en_US/messages.json": {}, + }, + }); + + equal(extension.errors.length, 0, "No errors reported"); + + yield extension.initAllLocales(); + + equal(extension.errors.length, 1, "One error reported"); + + do_print(`Got error: ${extension.errors[0]}`); + + ok(extension.errors[0].includes('"default_locale" property is required'), + "Got missing default_locale error"); +}); + + +add_task(function* testInvalidDefaultLocale() { + let extension = yield generateAddon({ + "manifest": { + "default_locale": "en", + }, + + "files": { + "_locales/en_US/messages.json": {}, + }, + }); + + equal(extension.errors.length, 1, "One error reported"); + + do_print(`Got error: ${extension.errors[0]}`); + + ok(extension.errors[0].includes("Loading locale file _locales/en/messages.json"), + "Got invalid default_locale error"); + + yield extension.initAllLocales(); + + equal(extension.errors.length, 2, "Two errors reported"); + + do_print(`Got error: ${extension.errors[1]}`); + + ok(extension.errors[1].includes('"default_locale" property must correspond'), + "Got invalid default_locale error"); +}); + + +add_task(function* testUnexpectedDefaultLocale() { + let extension = yield generateAddon({ + "manifest": { + "default_locale": "en_US", + }, + }); + + equal(extension.errors.length, 1, "One error reported"); + + do_print(`Got error: ${extension.errors[0]}`); + + ok(extension.errors[0].includes("Loading locale file _locales/en-US/messages.json"), + "Got invalid default_locale error"); + + yield extension.initAllLocales(); + + equal(extension.errors.length, 2, "One error reported"); + + do_print(`Got error: ${extension.errors[1]}`); + + ok(extension.errors[1].includes('"default_locale" property must correspond'), + "Got unexpected default_locale error"); +}); + + +add_task(function* testInvalidSyntax() { + let extension = yield generateAddon({ + "manifest": { + "default_locale": "en_US", + }, + + "files": { + "_locales/en_US/messages.json": '{foo: {message: "bar", description: "baz"}}', + }, + }); + + equal(extension.errors.length, 1, "No errors reported"); + + do_print(`Got error: ${extension.errors[0]}`); + + ok(extension.errors[0].includes("Loading locale file _locales\/en_US\/messages\.json: SyntaxError"), + "Got syntax error"); + + yield extension.initAllLocales(); + + equal(extension.errors.length, 2, "One error reported"); + + do_print(`Got error: ${extension.errors[1]}`); + + ok(extension.errors[1].includes("Loading locale file _locales\/en_US\/messages\.json: SyntaxError"), + "Got syntax error"); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_native_messaging.js b/toolkit/components/extensions/test/xpcshell/test_native_messaging.js new file mode 100644 index 000000000..1fcb7799e --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_native_messaging.js @@ -0,0 +1,302 @@ +"use strict"; + +/* global OS, HostManifestManager, NativeApp */ +Cu.import("resource://gre/modules/AppConstants.jsm"); +Cu.import("resource://gre/modules/AsyncShutdown.jsm"); +Cu.import("resource://gre/modules/ExtensionCommon.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/Schemas.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +const {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm"); +Cu.import("resource://gre/modules/NativeMessaging.jsm"); +Cu.import("resource://gre/modules/osfile.jsm"); + +let registry = null; +if (AppConstants.platform == "win") { + Cu.import("resource://testing-common/MockRegistry.jsm"); + registry = new MockRegistry(); + do_register_cleanup(() => { + registry.shutdown(); + }); +} + +const REGPATH = "Software\\Mozilla\\NativeMessagingHosts"; + +const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json"; + +let dir = FileUtils.getDir("TmpD", ["NativeMessaging"]); +dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + +let userDir = dir.clone(); +userDir.append("user"); +userDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + +let globalDir = dir.clone(); +globalDir.append("global"); +globalDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + +let dirProvider = { + getFile(property) { + if (property == "XREUserNativeMessaging") { + return userDir.clone(); + } else if (property == "XRESysNativeMessaging") { + return globalDir.clone(); + } + return null; + }, +}; + +Services.dirsvc.registerProvider(dirProvider); + +do_register_cleanup(() => { + Services.dirsvc.unregisterProvider(dirProvider); + dir.remove(true); +}); + +function writeManifest(path, manifest) { + if (typeof manifest != "string") { + manifest = JSON.stringify(manifest); + } + return OS.File.writeAtomic(path, manifest); +} + +let PYTHON; +add_task(function* setup() { + yield Schemas.load(BASE_SCHEMA); + + PYTHON = yield Subprocess.pathSearch("python2.7"); + if (PYTHON == null) { + PYTHON = yield Subprocess.pathSearch("python"); + } + notEqual(PYTHON, null, "Found a suitable python interpreter"); +}); + +let global = this; + +// Test of HostManifestManager.lookupApplication() begin here... +let context = { + url: null, + jsonStringify(...args) { return JSON.stringify(...args); }, + cloneScope: global, + logError() {}, + preprocessors: {}, + callOnClose: () => {}, + forgetOnClose: () => {}, +}; + +class MockContext extends ExtensionCommon.BaseContext { + constructor(extensionId) { + let fakeExtension = {id: extensionId}; + super("testEnv", fakeExtension); + this.sandbox = Cu.Sandbox(global); + } + + get cloneScope() { + return global; + } + + get principal() { + return Cu.getObjectPrincipal(this.sandbox); + } +} + +let templateManifest = { + name: "test", + description: "this is only a test", + path: "/bin/cat", + type: "stdio", + allowed_extensions: ["extension@tests.mozilla.org"], +}; + +add_task(function* test_nonexistent_manifest() { + let result = yield HostManifestManager.lookupApplication("test", context); + equal(result, null, "lookupApplication returns null for non-existent application"); +}); + +const USER_TEST_JSON = OS.Path.join(userDir.path, "test.json"); + +add_task(function* test_good_manifest() { + yield writeManifest(USER_TEST_JSON, templateManifest); + if (registry) { + registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + `${REGPATH}\\test`, "", USER_TEST_JSON); + } + + let result = yield HostManifestManager.lookupApplication("test", context); + notEqual(result, null, "lookupApplication finds a good manifest"); + equal(result.path, USER_TEST_JSON, "lookupApplication returns the correct path"); + deepEqual(result.manifest, templateManifest, "lookupApplication returns the manifest contents"); +}); + +add_task(function* test_invalid_json() { + yield writeManifest(USER_TEST_JSON, "this is not valid json"); + let result = yield HostManifestManager.lookupApplication("test", context); + equal(result, null, "lookupApplication ignores bad json"); +}); + +add_task(function* test_invalid_name() { + let manifest = Object.assign({}, templateManifest); + manifest.name = "../test"; + yield writeManifest(USER_TEST_JSON, manifest); + let result = yield HostManifestManager.lookupApplication("test", context); + equal(result, null, "lookupApplication ignores an invalid name"); +}); + +add_task(function* test_name_mismatch() { + let manifest = Object.assign({}, templateManifest); + manifest.name = "not test"; + yield writeManifest(USER_TEST_JSON, manifest); + let result = yield HostManifestManager.lookupApplication("test", context); + let what = (AppConstants.platform == "win") ? "registry key" : "json filename"; + equal(result, null, `lookupApplication ignores mistmatch between ${what} and name property`); +}); + +add_task(function* test_missing_props() { + const PROPS = [ + "name", + "description", + "path", + "type", + "allowed_extensions", + ]; + for (let prop of PROPS) { + let manifest = Object.assign({}, templateManifest); + delete manifest[prop]; + + yield writeManifest(USER_TEST_JSON, manifest); + let result = yield HostManifestManager.lookupApplication("test", context); + equal(result, null, `lookupApplication ignores missing ${prop}`); + } +}); + +add_task(function* test_invalid_type() { + let manifest = Object.assign({}, templateManifest); + manifest.type = "bogus"; + yield writeManifest(USER_TEST_JSON, manifest); + let result = yield HostManifestManager.lookupApplication("test", context); + equal(result, null, "lookupApplication ignores invalid type"); +}); + +add_task(function* test_no_allowed_extensions() { + let manifest = Object.assign({}, templateManifest); + manifest.allowed_extensions = []; + yield writeManifest(USER_TEST_JSON, manifest); + let result = yield HostManifestManager.lookupApplication("test", context); + equal(result, null, "lookupApplication ignores manifest with no allowed_extensions"); +}); + +const GLOBAL_TEST_JSON = OS.Path.join(globalDir.path, "test.json"); +let globalManifest = Object.assign({}, templateManifest); +globalManifest.description = "This manifest is from the systemwide directory"; + +add_task(function* good_manifest_system_dir() { + yield OS.File.remove(USER_TEST_JSON); + yield writeManifest(GLOBAL_TEST_JSON, globalManifest); + if (registry) { + registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + `${REGPATH}\\test`, "", null); + registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, + `${REGPATH}\\test`, "", GLOBAL_TEST_JSON); + } + + let where = (AppConstants.platform == "win") ? "registry location" : "directory"; + let result = yield HostManifestManager.lookupApplication("test", context); + notEqual(result, null, `lookupApplication finds a manifest in the system-wide ${where}`); + equal(result.path, GLOBAL_TEST_JSON, `lookupApplication returns path in the system-wide ${where}`); + deepEqual(result.manifest, globalManifest, `lookupApplication returns manifest contents from the system-wide ${where}`); +}); + +add_task(function* test_user_dir_precedence() { + yield writeManifest(USER_TEST_JSON, templateManifest); + if (registry) { + registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + `${REGPATH}\\test`, "", USER_TEST_JSON); + } + // global test.json and LOCAL_MACHINE registry key on windows are + // still present from the previous test + + let result = yield HostManifestManager.lookupApplication("test", context); + notEqual(result, null, "lookupApplication finds a manifest when entries exist in both user-specific and system-wide locations"); + equal(result.path, USER_TEST_JSON, "lookupApplication returns the user-specific path when user-specific and system-wide entries both exist"); + deepEqual(result.manifest, templateManifest, "lookupApplication returns user-specific manifest contents with user-specific and system-wide entries both exist"); +}); + +// Test shutdown handling in NativeApp +add_task(function* test_native_app_shutdown() { + const SCRIPT = String.raw` +import signal +import struct +import sys + +signal.signal(signal.SIGTERM, signal.SIG_IGN) + +while True: + rawlen = sys.stdin.read(4) + if len(rawlen) == 0: + signal.pause() + msglen = struct.unpack('@I', rawlen)[0] + msg = sys.stdin.read(msglen) + + sys.stdout.write(struct.pack('@I', msglen)) + sys.stdout.write(msg) + `; + + let scriptPath = OS.Path.join(userDir.path, "wontdie.py"); + let manifestPath = OS.Path.join(userDir.path, "wontdie.json"); + + const ID = "native@tests.mozilla.org"; + let manifest = { + name: "wontdie", + description: "test async shutdown of native apps", + type: "stdio", + allowed_extensions: [ID], + }; + + if (AppConstants.platform == "win") { + yield OS.File.writeAtomic(scriptPath, SCRIPT); + + let batPath = OS.Path.join(userDir.path, "wontdie.bat"); + let batBody = `@ECHO OFF\n${PYTHON} -u "${scriptPath}" %*\n`; + yield OS.File.writeAtomic(batPath, batBody); + yield OS.File.setPermissions(batPath, {unixMode: 0o755}); + + manifest.path = batPath; + yield writeManifest(manifestPath, manifest); + + registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + `${REGPATH}\\wontdie`, "", manifestPath); + } else { + yield OS.File.writeAtomic(scriptPath, `#!${PYTHON} -u\n${SCRIPT}`); + yield OS.File.setPermissions(scriptPath, {unixMode: 0o755}); + manifest.path = scriptPath; + yield writeManifest(manifestPath, manifest); + } + + let mockContext = new MockContext(ID); + let app = new NativeApp(mockContext, "wontdie"); + + // send a message and wait for the reply to make sure the app is running + let MSG = "test"; + let recvPromise = new Promise(resolve => { + let listener = (what, msg) => { + equal(msg, MSG, "Received test message"); + app.off("message", listener); + resolve(); + }; + app.on("message", listener); + }); + + let buffer = NativeApp.encodeMessage(mockContext, MSG); + app.send(buffer); + yield recvPromise; + + app._cleanup(); + + do_print("waiting for async shutdown"); + Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true); + AsyncShutdown.profileBeforeChange._trigger(); + Services.prefs.clearUserPref("toolkit.asyncshutdown.testing"); + + let procs = yield SubprocessImpl.Process.getWorker().call("getProcesses", []); + equal(procs.size, 0, "native process exited"); +}); diff --git a/toolkit/components/extensions/test/xpcshell/xpcshell.ini b/toolkit/components/extensions/test/xpcshell/xpcshell.ini new file mode 100644 index 000000000..3d0198ee9 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini @@ -0,0 +1,72 @@ +[DEFAULT] +head = head.js +tail = +firefox-appdir = browser +skip-if = appname == "thunderbird" +support-files = + data/** head_sync.js +tags = webextensions + +[test_csp_custom_policies.js] +[test_csp_validator.js] +[test_ext_alarms.js] +[test_ext_alarms_does_not_fire.js] +[test_ext_alarms_periodic.js] +[test_ext_alarms_replaces.js] +[test_ext_apimanager.js] +[test_ext_api_permissions.js] +[test_ext_background_generated_load_events.js] +[test_ext_background_generated_reload.js] +[test_ext_background_global_history.js] +skip-if = os == "android" # Android does not use Places for history. +[test_ext_background_private_browsing.js] +[test_ext_background_runtime_connect_params.js] +[test_ext_background_sub_windows.js] +[test_ext_background_window_properties.js] +skip-if = os == "android" +[test_ext_contexts.js] +[test_ext_downloads.js] +[test_ext_downloads_download.js] +skip-if = os == "android" +[test_ext_downloads_misc.js] +skip-if = os == "android" +[test_ext_downloads_search.js] +skip-if = os == "android" +[test_ext_experiments.js] +skip-if = release_or_beta +[test_ext_extension.js] +[test_ext_idle.js] +[test_ext_json_parser.js] +[test_ext_localStorage.js] +[test_ext_management.js] +[test_ext_management_uninstall_self.js] +[test_ext_manifest_content_security_policy.js] +[test_ext_manifest_incognito.js] +[test_ext_manifest_minimum_chrome_version.js] +[test_ext_onmessage_removelistener.js] +[test_ext_runtime_connect_no_receiver.js] +[test_ext_runtime_getBrowserInfo.js] +[test_ext_runtime_getPlatformInfo.js] +[test_ext_runtime_onInstalled_and_onStartup.js] +[test_ext_runtime_sendMessage.js] +[test_ext_runtime_sendMessage_errors.js] +[test_ext_runtime_sendMessage_no_receiver.js] +[test_ext_runtime_sendMessage_self.js] +[test_ext_schemas.js] +[test_ext_schemas_api_injection.js] +[test_ext_schemas_async.js] +[test_ext_schemas_allowed_contexts.js] +[test_ext_simple.js] +[test_ext_storage.js] +[test_ext_storage_sync.js] +head = head.js head_sync.js +skip-if = os == "android" +[test_ext_topSites.js] +skip-if = os == "android" +[test_getAPILevelForWindow.js] +[test_ext_legacy_extension_context.js] +[test_ext_legacy_extension_embedding.js] +[test_locale_converter.js] +[test_locale_data.js] +[test_native_messaging.js] +skip-if = os == "android" |