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 /testing/web-platform/tests/mixed-content/generic | |
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 'testing/web-platform/tests/mixed-content/generic')
17 files changed, 1202 insertions, 0 deletions
diff --git a/testing/web-platform/tests/mixed-content/generic/common.js b/testing/web-platform/tests/mixed-content/generic/common.js new file mode 100644 index 000000000..36427a466 --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/common.js @@ -0,0 +1,398 @@ +/** + * @fileoverview Utilities for mixed-content in Web Platform Tests. + * @author burnik@google.com (Kristijan Burnik) + * Disclaimer: Some methods of other authors are annotated in the corresponding + * method's JSDoc. + */ + +/** + * Normalizes the target port for use in a URL. For default ports, this is the + * empty string (omitted port), otherwise it's a colon followed by the port + * number. Ports 80, 443 and an empty string are regarded as default ports. + * @param {number} targetPort The port to use + * @return {string} The port portion for using as part of a URL. + */ +function getNormalizedPort(targetPort) { + return ([80, 443, ""].indexOf(targetPort) >= 0) ? "" : ":" + targetPort; +} + +/** + * Creates a GUID. + * See: https://en.wikipedia.org/wiki/Globally_unique_identifier + * Original author: broofa (http://www.broofa.com/) + * Sourced from: http://stackoverflow.com/a/2117523/4949715 + * @return {string} A pseudo-random GUID. + */ +function guid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +/** + * Initiates a new XHR via GET. + * @param {string} url The endpoint URL for the XHR. + * @param {string} responseType Optional - how should the response be parsed. + * Default is "json". + * See: https://xhr.spec.whatwg.org/#dom-xmlhttprequest-responsetype + * @return {Promise} A promise wrapping the success and error events. + */ +function xhrRequest(url, responseType) { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = responseType || "json"; + + xhr.addEventListener("error", function() { + reject(Error("Network Error")); + }); + + xhr.addEventListener("load", function() { + if (xhr.status != 200) + return reject(Error(xhr.statusText)); + + resolve(xhr.response); + }); + + xhr.send(); + }); +} + +/** + * Sets attributes on a given DOM element. + * @param {DOMElement} element The element on which to set the attributes. + * @param {object} An object with keys (serving as attribute names) and values. + */ +function setAttributes(el, attrs) { + attrs = attrs || {} + for (var attr in attrs) + el.setAttribute(attr, attrs[attr]); +} + + +/** + * Binds to success and error events of an object wrapping them into a promise + * available through {@code element.eventPromise}. The success event + * resolves and error event rejects. + * @param {object} element An object supporting events on which to bind the + * promise. + * @param {string} resolveEventName [="load"] The event name to bind resolve to. + * @param {string} rejectEventName [="error"] The event name to bind reject to. + */ +function bindEvents(element, resolveEventName, rejectEventName) { + element.eventPromise = new Promise(function(resolve, reject) { + element.addEventListener(resolveEventName || "load", resolve); + element.addEventListener(rejectEventName || "error", + function(e) { e.preventDefault(); reject(); } ); + }); +} + +/** + * Creates a new DOM element. + * @param {string} tagName The type of the DOM element. + * @param {object} attrs A JSON with attributes to apply to the element. + * @param {DOMElement} parent Optional - an existing DOM element to append to + * If not provided, the returned element will remain orphaned. + * @param {boolean} doBindEvents Optional - Whether to bind to load and error + * events and provide the promise wrapping the events via the element's + * {@code eventPromise} property. Default value evaluates to false. + * @return {DOMElement} The newly created DOM element. + */ +function createElement(tagName, attrs, parent, doBindEvents) { + var element = document.createElement(tagName); + + if (doBindEvents) + bindEvents(element); + + // We set the attributes after binding to events to catch any + // event-triggering attribute changes. E.g. form submission. + // + // But be careful with images: unlike other elements they will start the load + // as soon as the attr is set, even if not in the document yet, and sometimes + // complete it synchronously, so the append doesn't have the effect we want. + // So for images, we want to set the attrs after appending, whereas for other + // elements we want to do it before appending. + var isImg = (tagName == "img"); + if (!isImg) + setAttributes(element, attrs); + + if (parent) + parent.appendChild(element); + + if (isImg) + setAttributes(element, attrs); + + return element; +} + +function createRequestViaElement(tagName, attrs, parent) { + return createElement(tagName, attrs, parent, true).eventPromise; +} + +/** + * Creates a new empty iframe and appends it to {@code document.body} . + * @param {string} name The name and ID of the new iframe. + * @param {boolean} doBindEvents Whether to bind load and error events. + * @return {DOMElement} The newly created iframe. + */ +function createHelperIframe(name, doBindEvents) { + return createElement("iframe", + {"name": name, "id": name}, + document.body, + doBindEvents); +} + +/** + * Creates a new iframe, binds load and error events, sets the src attribute and + * appends it to {@code document.body} . + * @param {string} url The src for the iframe. + * @return {Promise} The promise for success/error events. + */ +function requestViaIframe(url) { + return createRequestViaElement("iframe", {"src": url}, document.body); +} + +/** + * Creates a new image, binds load and error events, sets the src attribute and + * appends it to {@code document.body} . + * @param {string} url The src for the image. + * @return {Promise} The promise for success/error events. + */ +function requestViaImage(url) { + return createRequestViaElement("img", {"src": url}, document.body); +} + +/** + * Initiates a new XHR GET request to provided URL. + * @param {string} url The endpoint URL for the XHR. + * @return {Promise} The promise for success/error events. + */ +function requestViaXhr(url) { + return xhrRequest(url); +} + +/** + * Initiates a new GET request to provided URL via the Fetch API. + * @param {string} url The endpoint URL for the Fetch. + * @return {Promise} The promise for success/error events. + */ +function requestViaFetch(url) { + return fetch(url); +} + +/** + * Creates a new Worker, binds message and error events wrapping them into. + * {@code worker.eventPromise} and posts an empty string message to start + * the worker. + * @param {string} url The endpoint URL for the worker script. + * @return {Promise} The promise for success/error events. + */ +function requestViaWorker(url) { + var worker = new Worker(url); + bindEvents(worker, "message", "error"); + worker.postMessage(''); + + return worker.eventPromise; +} + +/** + * Sets the href attribute on a navigable DOM element and performs a navigation + * by clicking it. To avoid navigating away from the current execution + * context, a target attribute is set to point to a new helper iframe. + * @param {DOMElement} navigableElement The navigable DOMElement + * @param {string} url The href for the navigable element. + * @return {Promise} The promise for success/error events. + */ +function requestViaNavigable(navigableElement, url) { + var iframe = createHelperIframe(guid(), true); + setAttributes(navigableElement, + {"href": url, + "target": iframe.name}); + navigableElement.click(); + + return iframe.eventPromise; +} + +/** + * Creates a new anchor element, appends it to {@code document.body} and + * performs the navigation. + * @param {string} url The URL to navigate to. + * @return {Promise} The promise for success/error events. + */ +function requestViaAnchor(url) { + var a = createElement("a", {"innerHTML": "Link to resource"}, document.body); + + return requestViaNavigable(a, url); +} + +/** + * Creates a new area element, appends it to {@code document.body} and performs + * the navigation. + * @param {string} url The URL to navigate to. + * @return {Promise} The promise for success/error events. + */ +function requestViaArea(url) { + var area = createElement("area", {}, document.body); + + return requestViaNavigable(area, url); +} + +/** + * Creates a new script element, sets the src to url, and appends it to + * {@code document.body}. + * @param {string} url The src URL. + * @return {Promise} The promise for success/error events. + */ +function requestViaScript(url) { + return createRequestViaElement("script", {"src": url}, document.body); +} + +/** + * Creates a new form element, sets attributes, appends it to + * {@code document.body} and submits the form. + * @param {string} url The URL to submit to. + * @return {Promise} The promise for success/error events. + */ +function requestViaForm(url) { + var iframe = createHelperIframe(guid()); + var form = createElement("form", + {"action": url, + "method": "POST", + "target": iframe.name}, + document.body); + bindEvents(iframe); + form.submit(); + + return iframe.eventPromise; +} + +/** + * Creates a new link element for a stylesheet, binds load and error events, + * sets the href to url and appends it to {@code document.head}. + * @param {string} url The URL for a stylesheet. + * @return {Promise} The promise for success/error events. + */ +function requestViaLinkStylesheet(url) { + return createRequestViaElement("link", + {"rel": "stylesheet", "href": url}, + document.head); +} + +/** + * Creates a new link element for a prefetch, binds load and error events, sets + * the href to url and appends it to {@code document.head}. + * @param {string} url The URL of a resource to prefetch. + * @return {Promise} The promise for success/error events. + */ +function requestViaLinkPrefetch(url) { + // TODO(kristijanburnik): Check if prefetch should support load and error + // events. For now we assume it's not specified. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Link_prefetching_FAQ + return createRequestViaElement("link", + {"rel": "prefetch", "href": url}, + document.head); +} + +/** + * Creates a new media element with a child source element, binds loadeddata and + * error events, sets attributes and appends to document.body. + * @param {string} type The type of the media element (audio/video/picture). + * @param {object} media_attrs The attributes for the media element. + * @param {object} source_attrs The attributes for the child source element. + * @return {DOMElement} The newly created media element. + */ +function createMediaElement(type, media_attrs, source_attrs) { + var mediaElement = createElement(type, {}); + var sourceElement = createElement("source", {}, mediaElement); + + mediaElement.eventPromise = new Promise(function(resolve, reject) { + mediaElement.addEventListener("loadeddata", resolve); + // Notice that the source element will raise the error. + sourceElement.addEventListener("error", reject); + }); + + setAttributes(mediaElement, media_attrs); + setAttributes(sourceElement, source_attrs); + document.body.appendChild(mediaElement); + + return mediaElement; +} + +/** + * Creates a new video element, binds loadeddata and error events, sets + * attributes and source URL and appends to {@code document.body}. + * @param {string} url The URL of the video. + * @return {Promise} The promise for success/error events. + */ +function requestViaVideo(url) { + return createMediaElement("video", + {}, + {type: "video/mp4", src: url}).eventPromise; +} + +/** + * Creates a new audio element, binds loadeddata and error events, sets + * attributes and source URL and appends to {@code document.body}. + * @param {string} url The URL of the audio. + * @return {Promise} The promise for success/error events. + */ +function requestViaAudio(url) { + return createMediaElement("audio", + {}, + {type: "audio/mpeg", src: url}).eventPromise; +} + +/** + * Creates a new picture element, binds loadeddata and error events, sets + * attributes and source URL and appends to {@code document.body}. Also + * creates new image element appending it to the picture + * @param {string} url The URL of the image for the source and image elements. + * @return {Promise} The promise for success/error events. + */ +function requestViaPicture(url) { + var picture = createMediaElement("picture", {}, {"srcset": url, + "type": "image/png"}); + return createRequestViaElement("img", {"src": url}, picture); +} + +/** + * Creates a new object element, binds load and error events, sets the data to + * url, and appends it to {@code document.body}. + * @param {string} url The data URL. + * @return {Promise} The promise for success/error events. + */ +function requestViaObject(url) { + return createRequestViaElement("object", {"data": url}, document.body); +} + +/** + * Creates a new WebSocket pointing to {@code url} and sends a message string + * "echo". The {@code message} and {@code error} events are triggering the + * returned promise resolve/reject events. + * @param {string} url The URL for WebSocket to connect to. + * @return {Promise} The promise for success/error events. + */ +function requestViaWebSocket(url) { + return new Promise(function(resolve, reject) { + var websocket = new WebSocket(url); + + websocket.addEventListener("message", function(e) { + resolve(JSON.parse(e.data)); + }); + + websocket.addEventListener("open", function(e) { + websocket.send("echo"); + }); + + websocket.addEventListener("error", function(e) { + reject(e) + }); + }); +} + +// SanityChecker does nothing in release mode. See sanity-checker.js for debug +// mode. +function SanityChecker() {} +SanityChecker.prototype.checkScenario = function() {}; +SanityChecker.prototype.setFailTimeout = function(test, timeout) {}; diff --git a/testing/web-platform/tests/mixed-content/generic/expect.py b/testing/web-platform/tests/mixed-content/generic/expect.py new file mode 100644 index 000000000..a3ea61b21 --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/expect.py @@ -0,0 +1,102 @@ +import json, os, urllib, urlparse + +def redirect(url, response): + response.add_required_headers = False + response.writer.write_status(301) + response.writer.write_header("access-control-allow-origin", "*") + response.writer.write_header("location", url) + response.writer.end_headers() + response.writer.write("") + +def create_redirect_url(request, swap_scheme = False): + parsed = urlparse.urlsplit(request.url) + destination_netloc = parsed.netloc + scheme = parsed.scheme + + if swap_scheme: + scheme = "http" if parsed.scheme == "https" else "https" + hostname = parsed.netloc.split(':')[0] + port = request.server.config["ports"][scheme][0] + destination_netloc = ":".join([hostname, str(port)]) + + # Remove "redirection" from query to avoid redirect loops. + parsed_query = dict(urlparse.parse_qsl(parsed.query)) + assert "redirection" in parsed_query + del parsed_query["redirection"] + + destination_url = urlparse.urlunsplit(urlparse.SplitResult( + scheme = scheme, + netloc = destination_netloc, + path = parsed.path, + query = urllib.urlencode(parsed_query), + fragment = None)) + + return destination_url + +def main(request, response): + if "redirection" in request.GET: + redirection = request.GET["redirection"] + if redirection == "no-redirect": + pass + elif redirection == "keep-scheme-redirect": + redirect(create_redirect_url(request, swap_scheme=False), response) + return + elif redirection == "swap-scheme-redirect": + redirect(create_redirect_url(request, swap_scheme=True), response) + return + else: + raise ValueError ("Invalid redirect type: %s" % redirection) + + content_type = "text/plain" + response_data = "" + + if "action" in request.GET: + action = request.GET["action"] + + if "content_type" in request.GET: + content_type = request.GET["content_type"] + + key = request.GET["key"] + stash = request.server.stash + path = request.GET.get("path", request.url.split('?'))[0] + + if action == "put": + value = request.GET["value"] + stash.take(key=key, path=path) + stash.put(key=key, value=value, path=path) + response_data = json.dumps({"status": "success", "result": key}) + elif action == "purge": + value = stash.take(key=key, path=path) + if content_type == "image/png": + response_data = open(os.path.join(request.doc_root, + "images", + "smiley.png"), "rb").read() + elif content_type == "audio/mpeg": + response_data = open(os.path.join(request.doc_root, + "media", + "sound_5.oga"), "rb").read() + elif content_type == "video/mp4": + response_data = open(os.path.join(request.doc_root, + "media", + "movie_5.mp4"), "rb").read() + elif content_type == "application/javascript": + response_data = open(os.path.join(request.doc_root, + "mixed-content", + "generic", + "worker.js"), "rb").read() + else: + response_data = "/* purged */" + elif action == "take": + value = stash.take(key=key, path=path) + if value is None: + status = "allowed" + else: + status = "blocked" + response_data = json.dumps({"status": status, "result": value}) + + response.add_required_headers = False + response.writer.write_status(200) + response.writer.write_header("content-type", content_type) + response.writer.write_header("cache-control", "no-cache; must-revalidate") + response.writer.end_headers() + response.writer.write(response_data) diff --git a/testing/web-platform/tests/mixed-content/generic/mixed-content-test-case.js b/testing/web-platform/tests/mixed-content/generic/mixed-content-test-case.js new file mode 100644 index 000000000..7f3a7bfa9 --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/mixed-content-test-case.js @@ -0,0 +1,163 @@ +/** + * @fileoverview Test case for mixed-content in Web Platform Tests. + * @author burnik@google.com (Kristijan Burnik) + */ + +/** + * MixedContentTestCase exercises all the tests for checking browser behavior + * when resources regarded as mixed-content are requested. A single run covers + * only a single scenario. + * @param {object} scenario A JSON describing the test arrangement and + * expectation(s). Refer to /mixed-content/spec.src.json for details. + * @param {string} description The test scenario verbose description. + * @param {SanityChecker} sanityChecker Instance of an object used to check the + * running scenario. Useful in debug mode. See ./sanity-checker.js. + * Run {@code ./tools/generate.py -h} for info on test generating modes. + * @return {object} Object wrapping the start method used to run the test. + */ +function MixedContentTestCase(scenario, description, sanityChecker) { + var httpProtocol = "http"; + var httpsProtocol = "https"; + var wsProtocol = "ws"; + var wssProtocol = "wss"; + + var sameOriginHost = location.hostname; + var crossOriginHost = "{{domains[www1]}}"; + + // These values can evaluate to either empty strings or a ":port" string. + var httpPort = getNormalizedPort(parseInt("{{ports[http][0]}}", 10)); + var httpsPort = getNormalizedPort(parseInt("{{ports[https][0]}}", 10)); + var wsPort = getNormalizedPort(parseInt("{{ports[ws][0]}}", 10)); + var wssPort = getNormalizedPort(parseInt("{{ports[wss][0]}}", 10)); + + var resourcePath = "/mixed-content/generic/expect.py"; + var wsResourcePath = "/stash_responder"; + + // Map all endpoints to scenario for use in the test. + var endpoint = { + "same-origin": + location.origin + resourcePath, + "same-host-https": + httpsProtocol + "://" + sameOriginHost + httpsPort + resourcePath, + "same-host-http": + httpProtocol + "://" + sameOriginHost + httpPort + resourcePath, + "cross-origin-https": + httpsProtocol + "://" + crossOriginHost + httpsPort + resourcePath, + "cross-origin-http": + httpProtocol + "://" + crossOriginHost + httpPort + resourcePath, + "same-host-wss": + wssProtocol + "://" + sameOriginHost + wssPort + wsResourcePath, + "same-host-ws": + wsProtocol + "://" + sameOriginHost + wsPort + wsResourcePath, + "cross-origin-wss": + wssProtocol + "://" + crossOriginHost + wssPort + wsResourcePath, + "cross-origin-ws": + wsProtocol + "://" + crossOriginHost + wsPort + wsResourcePath + }; + + // Mapping all the resource requesting methods to the scenario. + var resourceMap = { + "a-tag": requestViaAnchor, + "area-tag": requestViaArea, + "fetch-request": requestViaFetch, + "form-tag": requestViaForm, + "iframe-tag": requestViaIframe, + "img-tag": requestViaImage, + "script-tag": requestViaScript, + "worker-request": requestViaWorker, + "xhr-request": requestViaXhr, + "audio-tag": requestViaAudio, + "video-tag": requestViaVideo, + "picture-tag": requestViaPicture, + "object-tag": requestViaObject, + "link-css-tag": requestViaLinkStylesheet, + "link-prefetch-tag": requestViaLinkPrefetch, + "websocket-request": requestViaWebSocket + }; + + sanityChecker.checkScenario(scenario, resourceMap); + + // Mapping all expected MIME types to the scenario. + var contentType = { + "a-tag": "text/html", + "area-tag": "text/html", + "fetch-request": "application/json", + "form-tag": "text/html", + "iframe-tag": "text/html", + "img-tag": "image/png", + "script-tag": "text/javascript", + "worker-request": "application/javascript", + "xhr-request": "application/json", + "audio-tag": "audio/mpeg", + "video-tag": "video/mp4", + "picture-tag": "image/png", + "object-tag": "text/html", + "link-css-tag": "text/css", + "link-prefetch-tag": "text/html", + "websocket-request": "application/json" + }; + + var mixed_content_test = async_test(description); + + function runTest() { + sanityChecker.setFailTimeout(mixed_content_test); + + var key = guid(); + var value = guid(); + // We use the same path for both HTTP/S and WS/S stash requests. + var stash_path = encodeURIComponent("/mixed-content"); + var announceResourceRequestUrl = endpoint['same-origin'] + + "?action=put&key=" + key + + "&value=" + value + + "&path=" + stash_path; + var assertResourceRequestUrl = endpoint['same-origin'] + + "?action=take&key=" + key + + "&path=" + stash_path; + var resourceRequestUrl = endpoint[scenario.origin] + "?redirection=" + + scenario.redirection + "&action=purge&key=" + key + + "&path=" + stash_path + "&content_type=" + + contentType[scenario.subresource]; + + xhrRequest(announceResourceRequestUrl) + .then(function(response) { + // Send out the real resource request. + // This should tear down the key if it's not blocked. + return resourceMap[scenario.subresource](resourceRequestUrl); + }) + .then(function() { + mixed_content_test.step(function() { + assert_equals("allowed", scenario.expectation, + "The triggered event should match '" + + scenario.expectation + "'."); + }, "Check if success event was triggered."); + + // Send request to check if the key has been torn down. + return xhrRequest(assertResourceRequestUrl); + }, function(error) { + mixed_content_test.step(function() { + assert_equals("blocked", scenario.expectation, + "The triggered event should match '" + + scenario.expectation + "'."); + // TODO(kristijanburnik): param "error" can be an event or error. + // Map assertion by resource. + // e.g.: assert_equals(e.type, "error"); + }, "Check if error event was triggered."); + + // When requestResource fails, we also check the key state. + return xhrRequest(assertResourceRequestUrl); + }) + .then(function(response) { + // Now check if the value has been torn down. If it's still there, + // we have blocked the request to mixed-content. + mixed_content_test.step(function() { + assert_equals(response.status, scenario.expectation, + "The resource request should be '" + scenario.expectation + + "'."); + }, "Check if request was sent."); + mixed_content_test.done(); + }); + + } // runTest + + return {start: runTest}; +} // MixedContentTestCase diff --git a/testing/web-platform/tests/mixed-content/generic/sanity-checker.js b/testing/web-platform/tests/mixed-content/generic/sanity-checker.js new file mode 100644 index 000000000..55a103adf --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/sanity-checker.js @@ -0,0 +1,53 @@ +// The SanityChecker is used in debug mode to identify problems with the +// structure of the testsuite and to force early test failures. +// In release mode it is mocked out to do nothing. +function SanityChecker() {} + +SanityChecker.prototype.checkScenario = function(scenario, resourceInvoker) { + // Check if scenario is valid. + test(function() { + var expectedFields = SPEC_JSON["test_expansion_schema"]; + + for (var field in expectedFields) { + if (field == "expansion") + continue + + assert_own_property(scenario, field, + "The scenario should contain field '" + field + "'.") + + var expectedFieldList = expectedFields[field]; + if (!expectedFieldList.hasOwnProperty('length')) { + var expectedFieldList = []; + for (var key in expectedFields[field]) { + expectedFieldList = expectedFieldList.concat(expectedFields[field][key]) + } + } + assert_in_array(scenario[field], expectedFieldList, + "Scenario's " + field + " is one of: " + + expectedFieldList.join(", ")) + "." + } + + // Check if the protocol is matched. + assert_equals(scenario["source_scheme"] + ":", location.protocol, + "Protocol of the test page should match the scenario.") + + assert_own_property(resourceInvoker, scenario.subresource, + "Subresource should be supported"); + + }, "[MixedContentTestCase] The test scenario should be valid."); +} + +// For easier debugging runs, we can fail a test earlier. +SanityChecker.prototype.setFailTimeout = function(test, timeout) { + // Due to missing implementations, tests time out, so we fail them early. + // TODO(kristijanburnik): Once WPT rolled in: + // https://github.com/w3c/testharness.js/pull/127 + // Refactor to make use of step_timeout. + setTimeout(function() { + test.step(function() { + assert_equals(test.phase, test.phases.COMPLETE, + "Expected test to complete."); + test.done(); + }) + }, timeout || 1000); +} diff --git a/testing/web-platform/tests/mixed-content/generic/template/disclaimer.template b/testing/web-platform/tests/mixed-content/generic/template/disclaimer.template new file mode 100644 index 000000000..66c43ed6f --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/template/disclaimer.template @@ -0,0 +1 @@ +<!-- DO NOT EDIT! Generated by %(generating_script_filename)s using %(html_template_filename)s. --> diff --git a/testing/web-platform/tests/mixed-content/generic/template/spec_json.js.template b/testing/web-platform/tests/mixed-content/generic/template/spec_json.js.template new file mode 100644 index 000000000..e4cbd0342 --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/template/spec_json.js.template @@ -0,0 +1 @@ +var SPEC_JSON = %(spec_json)s; diff --git a/testing/web-platform/tests/mixed-content/generic/template/test.debug.html.template b/testing/web-platform/tests/mixed-content/generic/template/test.debug.html.template new file mode 100644 index 000000000..013bb6250 --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/template/test.debug.html.template @@ -0,0 +1,31 @@ +<!DOCTYPE html> +%(generated_disclaimer)s +<html> + <head> + <title>Mixed-Content: %(spec_title)s</title> + <meta charset='utf-8'> + <meta name="description" content="%(spec_description)s"> + <meta name="assert" content="%(test_description)s">%(meta_opt_in)s + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <!-- Common global functions for mixed-content tests. --> + <script src="/mixed-content/generic/common.js"></script> + <!-- The original specification JSON for validating the scenario. --> + <script src="/mixed-content/spec_json.js"></script> + <!-- Internal checking of the tests --> + <script src="/mixed-content/generic/sanity-checker.js"></script> + <!-- Simple wrapper API for all mixed-content test cases. --> + <script src="/mixed-content/generic/mixed-content-test-case.js?pipe=sub"></script> + </head> + <body> + <h1>%(spec_title)s</h1> + <h2>%(spec_description)s</h2> + <pre>%(test_description)s</pre> + + <p>See <a href="%(spec_specification_url)s" target="_blank">specification</a> + details for this test.</p> + + <div id="log"></div> + <script>%(test_js)s</script> + </body> +</html> diff --git a/testing/web-platform/tests/mixed-content/generic/template/test.js.template b/testing/web-platform/tests/mixed-content/generic/template/test.js.template new file mode 100644 index 000000000..b8c0769fc --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/template/test.js.template @@ -0,0 +1,13 @@ +MixedContentTestCase( + { + "opt_in_method": "%(opt_in_method)s", + "origin": "%(origin)s", + "source_scheme": "%(source_scheme)s", + "context_nesting": "%(context_nesting)s", + "redirection": "%(redirection)s", + "subresource": "%(subresource)s", + "expectation": "%(expectation)s" + }, + document.querySelector("meta[name=assert]").content, + new SanityChecker() +).start(); diff --git a/testing/web-platform/tests/mixed-content/generic/template/test.release.html.template b/testing/web-platform/tests/mixed-content/generic/template/test.release.html.template new file mode 100644 index 000000000..ca77389cc --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/template/test.release.html.template @@ -0,0 +1,20 @@ +<!DOCTYPE html> +%(generated_disclaimer)s +<html> + <head> + <title>Mixed-Content: %(spec_title)s</title> + <meta charset='utf-8'> + <meta name="description" content="%(spec_description)s"> + <link rel="author" title="Kristijan Burnik" href="burnik@chromium.org"> + <link rel="help" href="%(spec_specification_url)s"> + <meta name="assert" content="%(test_description)s">%(meta_opt_in)s + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/mixed-content/generic/common.js"></script> + <script src="/mixed-content/generic/mixed-content-test-case.js?pipe=sub"></script> + </head> + <body> + <script>%(test_js)s</script> + <div id="log"></div> + </body> +</html> diff --git a/testing/web-platform/tests/mixed-content/generic/template/test_description.template b/testing/web-platform/tests/mixed-content/generic/template/test_description.template new file mode 100644 index 000000000..33dbcaa05 --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/template/test_description.template @@ -0,0 +1,7 @@ +opt_in_method: %(opt_in_method)s +origin: %(origin)s +source_scheme: %(source_scheme)s +context_nesting: %(context_nesting)s +redirection: %(redirection)s +subresource: %(subresource)s +expectation: %(expectation)s diff --git a/testing/web-platform/tests/mixed-content/generic/tools/__init__.py b/testing/web-platform/tests/mixed-content/generic/tools/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/tools/__init__.py diff --git a/testing/web-platform/tests/mixed-content/generic/tools/clean.py b/testing/web-platform/tests/mixed-content/generic/tools/clean.py new file mode 100755 index 000000000..9416f0b5b --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/tools/clean.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +import os, json +from common_paths import * +import spec_validator + +def rmtree(top): + top = os.path.abspath(top) + assert top != os.path.expanduser("~") + assert len(top) > len(os.path.expanduser("~")) + assert "web-platform-tests" in top + assert "mixed-content" in top + + for root, dirs, files in os.walk(top, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + + os.rmdir(top) + +def main(): + spec_json = load_spec_json(); + spec_validator.assert_valid_spec_json(spec_json) + + for spec in spec_json['specification']: + generated_dir = os.path.join(spec_directory, spec["name"]) + if (os.path.isdir(generated_dir)): + rmtree(generated_dir) + + if (os.path.isfile(generated_spec_json_filename)): + os.remove(generated_spec_json_filename) + +if __name__ == '__main__': + main() diff --git a/testing/web-platform/tests/mixed-content/generic/tools/common_paths.py b/testing/web-platform/tests/mixed-content/generic/tools/common_paths.py new file mode 100644 index 000000000..5c2807d28 --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/tools/common_paths.py @@ -0,0 +1,58 @@ +import os, sys, json, re + +script_directory = os.path.dirname(os.path.abspath(__file__)) +generic_directory = os.path.abspath(os.path.join(script_directory, '..')) + +template_directory = os.path.abspath(os.path.join(script_directory, + '..', + 'template')) +spec_directory = os.path.abspath(os.path.join(script_directory, '..', '..')) +test_root_directory = os.path.abspath(os.path.join(script_directory, + '..', '..', '..')) + +spec_filename = os.path.join(spec_directory, "spec.src.json") +generated_spec_json_filename = os.path.join(spec_directory, "spec_json.js") + +selection_pattern = '%(opt_in_method)s/' + \ + '%(origin)s/' + \ + '%(subresource)s/' + \ + '%(context_nesting)s/' + \ + '%(redirection)s/' + +test_file_path_pattern = '%(spec_name)s/' + selection_pattern + \ + '%(name)s.%(source_scheme)s.html' + + +def get_template(basename): + with open(os.path.join(template_directory, basename), "r") as f: + return f.read() + + +def write_file(filename, contents): + with open(filename, "w") as f: + f.write(contents) + + +def read_nth_line(fp, line_number): + fp.seek(0) + for i, line in enumerate(fp): + if (i + 1) == line_number: + return line + + +def load_spec_json(path_to_spec = None): + if path_to_spec is None: + path_to_spec = spec_filename + + re_error_location = re.compile('line ([0-9]+) column ([0-9]+)') + with open(path_to_spec, "r") as f: + try: + return json.load(f) + except ValueError, ex: + print ex.message + match = re_error_location.search(ex.message) + if match: + line_number, column = int(match.group(1)), int(match.group(2)) + print read_nth_line(f, line_number).rstrip() + print " " * (column - 1) + "^" + sys.exit(1) diff --git a/testing/web-platform/tests/mixed-content/generic/tools/generate.py b/testing/web-platform/tests/mixed-content/generic/tools/generate.py new file mode 100755 index 000000000..6dcaebdc3 --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/tools/generate.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python + +import os, sys, json +from common_paths import * +import spec_validator +import argparse + + +def expand_pattern(expansion_pattern, test_expansion_schema): + expansion = {} + for artifact_key in expansion_pattern: + artifact_value = expansion_pattern[artifact_key] + if artifact_value == '*': + expansion[artifact_key] = test_expansion_schema[artifact_key] + elif isinstance(artifact_value, list): + expansion[artifact_key] = artifact_value + elif isinstance(artifact_value, dict): + # Flattened expansion. + expansion[artifact_key] = [] + values_dict = expand_pattern(artifact_value, + test_expansion_schema[artifact_key]) + for sub_key in values_dict.keys(): + expansion[artifact_key] += values_dict[sub_key] + else: + expansion[artifact_key] = [artifact_value] + + return expansion + + +def permute_expansion(expansion, artifact_order, selection = {}, artifact_index = 0): + assert isinstance(artifact_order, list), "artifact_order should be a list" + + if artifact_index >= len(artifact_order): + yield selection + return + + artifact_key = artifact_order[artifact_index] + + for artifact_value in expansion[artifact_key]: + selection[artifact_key] = artifact_value + for next_selection in permute_expansion(expansion, + artifact_order, + selection, + artifact_index + 1): + yield next_selection + + +def generate_selection(selection, spec, test_html_template_basename): + selection['spec_name'] = spec['name'] + selection['spec_title'] = spec['title'] + selection['spec_description'] = spec['description'] + selection['spec_specification_url'] = spec['specification_url'] + + test_filename = test_file_path_pattern % selection + test_headers_filename = test_filename + ".headers" + test_directory = os.path.dirname(test_filename) + full_path = os.path.join(spec_directory, test_directory) + + test_html_template = get_template(test_html_template_basename) + test_js_template = get_template("test.js.template") + disclaimer_template = get_template('disclaimer.template') + test_description_template = get_template("test_description.template") + + html_template_filename = os.path.join(template_directory, + test_html_template_basename) + generated_disclaimer = disclaimer_template \ + % {'generating_script_filename': os.path.relpath(__file__, + test_root_directory), + 'html_template_filename': os.path.relpath(html_template_filename, + test_root_directory)} + + selection['generated_disclaimer'] = generated_disclaimer.rstrip() + test_description_template = \ + test_description_template.rstrip().replace("\n", "\n" + " " * 33) + selection['test_description'] = test_description_template % selection + + # Adjust the template for the test invoking JS. Indent it to look nice. + indent = "\n" + " " * 6; + test_js_template = indent + test_js_template.replace("\n", indent); + selection['test_js'] = test_js_template % selection + + # Directory for the test files. + try: + os.makedirs(full_path) + except: + pass + + # TODO(kristijanburnik): Implement the opt-in-method here. + opt_in_method = selection['opt_in_method'] + selection['meta_opt_in'] = '' + if opt_in_method == 'meta-csp': + selection['meta_opt_in'] = '\n <meta http-equiv="Content-Security-Policy" ' + \ + 'content="block-all-mixed-content">' + elif opt_in_method == 'http-csp': + opt_in_headers = "Content-Security-Policy: block-all-mixed-content\n" + write_file(test_headers_filename, opt_in_headers) + elif opt_in_method == 'no-opt-in': + pass + else: + raise ValueError("Invalid opt_in_method %s" % opt_in_method) + + # Write out the generated HTML file. + write_file(test_filename, test_html_template % selection) + +def generate_test_source_files(spec_json, target): + test_expansion_schema = spec_json['test_expansion_schema'] + specification = spec_json['specification'] + + spec_json_js_template = get_template('spec_json.js.template') + write_file(generated_spec_json_filename, + spec_json_js_template % {'spec_json': json.dumps(spec_json)}) + + # Choose a debug/release template depending on the target. + html_template = "test.%s.html.template" % target + + artifact_order = test_expansion_schema.keys() + ['name'] + + # Create list of excluded tests. + exclusion_dict = {} + for excluded_pattern in spec_json['excluded_tests']: + excluded_expansion = \ + expand_pattern(excluded_pattern, + test_expansion_schema) + for excluded_selection in permute_expansion(excluded_expansion, artifact_order): + excluded_selection_path = selection_pattern % excluded_selection + exclusion_dict[excluded_selection_path] = True + + for spec in specification: + for expansion_pattern in spec['test_expansion']: + expansion = expand_pattern(expansion_pattern, + test_expansion_schema) + for selection in permute_expansion(expansion, artifact_order): + selection_path = selection_pattern % selection + if not selection_path in exclusion_dict: + generate_selection(selection, + spec, + html_template) + else: + print 'Excluding selection:', selection_path + + +def main(target, spec_filename): + spec_json = load_spec_json(spec_filename); + spec_validator.assert_valid_spec_json(spec_json) + generate_test_source_files(spec_json, target) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Test suite generator utility') + parser.add_argument('-t', '--target', type = str, + choices = ("release", "debug"), default = "release", + help = 'Sets the appropriate template for generating tests') + parser.add_argument('-s', '--spec', type = str, default = None, + help = 'Specify a file used for describing and generating the tests') + # TODO(kristijanburnik): Add option for the spec_json file. + args = parser.parse_args() + main(args.target, args.spec) diff --git a/testing/web-platform/tests/mixed-content/generic/tools/regenerate b/testing/web-platform/tests/mixed-content/generic/tools/regenerate new file mode 100755 index 000000000..e6bd63519 --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/tools/regenerate @@ -0,0 +1,3 @@ +#!/bin/bash +DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +python $DIR/clean.py && python $DIR/generate.py diff --git a/testing/web-platform/tests/mixed-content/generic/tools/spec_validator.py b/testing/web-platform/tests/mixed-content/generic/tools/spec_validator.py new file mode 100755 index 000000000..a6acc1040 --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/tools/spec_validator.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python + +import json, sys +from common_paths import * + +def assert_non_empty_string(obj, field): + assert field in obj, 'Missing field "%s"' % field + assert isinstance(obj[field], basestring), \ + 'Field "%s" must be a string' % field + assert len(obj[field]) > 0, 'Field "%s" must not be empty' % field + + +def assert_non_empty_list(obj, field): + assert isinstance(obj[field], list), \ + '%s must be a list' % field + assert len(obj[field]) > 0, \ + '%s list must not be empty' % field + + +def assert_non_empty_dict(obj, field): + assert isinstance(obj[field], dict), \ + '%s must be a dict' % field + assert len(obj[field]) > 0, \ + '%s dict must not be empty' % field + + +def assert_contains(obj, field): + assert field in obj, 'Must contain field "%s"' % field + + +def assert_string_from(obj, field, items): + assert obj[field] in items, \ + 'Field "%s" must be from: %s' % (field, str(items)) + + +def assert_string_or_list_items_from(obj, field, items): + if isinstance(obj[field], basestring): + assert_string_from(obj, field, items) + return + + assert isinstance(obj[field], list), "%s must be a list!" % field + for allowed_value in obj[field]: + assert allowed_value != '*', "Wildcard is not supported for lists!" + assert allowed_value in items, \ + 'Field "%s" must be from: %s' % (field, str(items)) + + +def assert_contains_only_fields(obj, expected_fields): + for expected_field in expected_fields: + assert_contains(obj, expected_field) + + for actual_field in obj: + assert actual_field in expected_fields, \ + 'Unexpected field "%s".' % actual_field + + +def assert_value_unique_in(value, used_values): + assert value not in used_values, 'Duplicate value "%s"!' % str(value) + used_values[value] = True + + +def assert_valid_artifact(exp_pattern, artifact_key, schema): + if isinstance(schema, list): + assert_string_or_list_items_from(exp_pattern, artifact_key, + ["*"] + schema) + return + + for sub_artifact_key, sub_schema in schema.iteritems(): + assert_valid_artifact(exp_pattern[artifact_key], sub_artifact_key, + sub_schema) + +def validate(spec_json, details): + """ Validates the json specification for generating tests. """ + + details['object'] = spec_json + assert_contains_only_fields(spec_json, ["specification", + "test_expansion_schema", + "excluded_tests"]) + assert_non_empty_list(spec_json, "specification") + assert_non_empty_dict(spec_json, "test_expansion_schema") + assert_non_empty_list(spec_json, "excluded_tests") + + specification = spec_json['specification'] + test_expansion_schema = spec_json['test_expansion_schema'] + excluded_tests = spec_json['excluded_tests'] + + valid_test_expansion_fields = ['name'] + test_expansion_schema.keys() + + # Validate each single spec. + for spec in specification: + details['object'] = spec + + # Validate required fields for a single spec. + assert_contains_only_fields(spec, ['name', + 'title', + 'description', + 'specification_url', + 'test_expansion']) + assert_non_empty_string(spec, 'name') + assert_non_empty_string(spec, 'title') + assert_non_empty_string(spec, 'description') + assert_non_empty_string(spec, 'specification_url') + assert_non_empty_list(spec, 'test_expansion') + + # Validate spec's test expansion. + used_spec_names = {} + + for spec_exp in spec['test_expansion']: + details['object'] = spec_exp + assert_non_empty_string(spec_exp, 'name') + # The name is unique in same expansion group. + assert_value_unique_in((spec_exp['expansion'], spec_exp['name']), + used_spec_names) + assert_contains_only_fields(spec_exp, valid_test_expansion_fields) + + for artifact in test_expansion_schema: + details['test_expansion_field'] = artifact + assert_valid_artifact(spec_exp, artifact, + test_expansion_schema[artifact]) + del details['test_expansion_field'] + + # Validate the test_expansion schema members. + details['object'] = test_expansion_schema + assert_contains_only_fields(test_expansion_schema, ['expansion', + 'source_scheme', + 'opt_in_method', + 'context_nesting', + 'redirection', + 'subresource', + 'origin', + 'expectation']) + # Validate excluded tests. + details['object'] = excluded_tests + for excluded_test_expansion in excluded_tests: + assert_contains_only_fields(excluded_test_expansion, + valid_test_expansion_fields) + + + del details['object'] + + +def assert_valid_spec_json(spec_json): + error_details = {} + try: + validate(spec_json, error_details) + except AssertionError, err: + print 'ERROR:', err.message + print json.dumps(error_details, indent=4) + sys.exit(1) + + +def main(): + spec_json = load_spec_json(); + assert_valid_spec_json(spec_json) + print "Spec JSON is valid." + + +if __name__ == '__main__': + main() diff --git a/testing/web-platform/tests/mixed-content/generic/worker.js b/testing/web-platform/tests/mixed-content/generic/worker.js new file mode 100644 index 000000000..7e2168bcc --- /dev/null +++ b/testing/web-platform/tests/mixed-content/generic/worker.js @@ -0,0 +1 @@ +postMessage('done'); |