Skip to content

Commit

Permalink
service worker: Add tests for inteception of workers after redirects.
Browse files Browse the repository at this point in the history
This tests behavior discussed here:
w3c/ServiceWorker#1289

Namely it tests when a request for a worker goes through a redirect
chain:
1) On redirect from A -> B, whether the service worker at B
sees the request.
2) After the final redirect, which service worker controls the
resulting client.

The tests are written as specified today. Therefore, Firefox
passes this test (verified in Nightly) and Chrome does not.
(Actually a small change is required to the test to make Firefox
pass it, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1452528)

Currently it only tests shared worker but dedicated worker can
be added in a follow-up patch.

Bug: 829720
Change-Id: Id3b1ea8b952760be0ef9917f2c6a3afe60ca1fb5
Reviewed-on: https://chromium-review.googlesource.com/999241
Commit-Queue: Matt Falkenhagen <[email protected]>
Reviewed-by: Hiroki Nakagawa <[email protected]>
Cr-Commit-Position: refs/heads/master@{#549125}
  • Loading branch information
mfalken authored and chromium-wpt-export-bot committed Apr 9, 2018
1 parent fc7c0b7 commit 6fe36d7
Show file tree
Hide file tree
Showing 6 changed files with 267 additions and 0 deletions.
5 changes: 5 additions & 0 deletions service-workers/service-worker/resources/scope1/redirect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import os
import sys
# Use the file from the parent directory.
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from redirect import main
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import os
import sys
# Use the file from the parent directory.
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from worker_interception_redirect_webworker import main
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
let name;
if (self.registration.scope.indexOf('scope1') != -1)
name = 'sw1';
if (self.registration.scope.indexOf('scope2') != -1)
name = 'sw2';


self.addEventListener('fetch', evt => {
// There are three types of requests this service worker handles.

// (1) The first request for the worker, which will redirect elsewhere.
// "redirect.py" means to test network redirect, so let network handle it.
if (evt.request.url.indexOf('redirect.py') != -1) {
return;
}
// "sw-redirect" means to test service worker redirect, so respond with a
// redirect.
if (evt.request.url.indexOf('sw-redirect') != -1) {
const url = new URL(evt.request.url);
const redirect_to = url.searchParams.get('Redirect');
evt.respondWith(Response.redirect(redirect_to));
return;
}

// (2) After redirect, the request is for a "webworker.py" URL.
// Add a search parameter to indicate this service worker handled the
// final request for the worker.
if (evt.request.url.indexOf('webworker.py') != -1) {
const greeting = encodeURIComponent(`${name} saw the request for the worker script`);
evt.respondWith(fetch(`worker_interception_redirect_webworker.py?greeting=${greeting}`));
return;
}

// (3) The worker does a fetch() to simple.txt. Indicate that this service
// worker handled the request.
if (evt.request.url.indexOf('simple.txt') != -1) {
evt.respondWith(new Response(`${name} saw the fetch from the worker`));
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// This is the (shared or dedicated) worker file for the
// worker-interception-redirect test. It should be served by the corresponding
// .py file instead of being served directly.
//
// This file is served from both resources/*webworker.py and
// resources/scope2/*webworker.py, hence some of the complexity
// below about paths.
const resources_url = new URL("/service-workers/service-worker/resources/",
self.location);

// This greeting text is meant to be injected by the Python script that serves
// this file, to indicate how the script was served (from network or from
// service worker).
//
// We can't just use a sub pipe and name this file .sub.js since we want
// to serve the file from multiple URLs (see above).
let greeting = '%GREETING_TEXT%';
if (!greeting)
greeting = 'the shared worker script was served from network';

self.onconnect = async function(e) {
const port = e.ports[0];
port.start();
port.postMessage(greeting);

const fetch_url = new URL('simple.txt', resources_url);
const response = await fetch(fetch_url);
const text = await response.text();
port.postMessage('fetch(): ' + text);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# This serves the worker JavaScript file. It takes a |greeting| request
# parameter to inject into the JavaScript to indicate how the request
# reached the server.
import os
import sys

def main(request, response):
path = os.path.join(os.path.dirname(__file__),
"worker-interception-redirect-webworker.js")
body = open(path, "rb").read()
if "greeting" in request.GET:
body = body.replace("%GREETING_TEXT%", request.GET["greeting"])
else:
body = body.replace("%GREETING_TEXT%", "")

headers = []
headers.append(("Content-Type", "text/javascript"))

return headers, body
169 changes: 169 additions & 0 deletions service-workers/service-worker/worker-interception-redirect.https.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<!DOCTYPE html>
<title>Service Worker: controlling a SharedWorker</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/test-helpers.sub.js"></script>
<body>
<script>
// This tests service worker interception for worker clients, when the request
// for the worker script goes through redirects. For example, a request can go
// through a chain of URLs like A -> B -> C -> D and each URL might fall in the
// scope of a different service worker, if any.
// The two key questions are:
// 1. Upon a redirect from A -> B, should a service worker for scope B
// intercept the request?
// 2. After the final response, which service worker controls the resulting
// client?
//
// The standard prescribes the following:
// 1. The service worker for scope B intercepts the redirect. *However*, once a
// request falls back to network (i.e., a service worker did not call
// respondWith()) and a redirect is then received from network, no service
// worker should intercept that redirect or any subsequent redirects.
// 2. The final service worker that got a fetch event (or would have, in the
// case of a non-fetch-event worker) becomes the controller of the client.
//
// The standard may change later, see:
// https://github.com/w3c/ServiceWorker/issues/1289
//
// The basic test setup is:
// 1. Page registers service workers for scope1 and scope2.
// 2. Page requests a worker from scope1.
// 3. The request is redirected to scope2 or out-of-scope.
// 4. The worker posts message to the page describing where the final response
// was served from (service worker or network).
// 5. The worker does a fetch(), and posts back the response, which describes
// where the fetch response was served from.
//
// Currently this only tests shared worker but dedicated worker tests should be
// added in a future patch.

// Globals for easier cleanup.
const scope1 = 'resources/scope1';
const scope2 = 'resources/scope2';
let frame;

function get_message_from_worker(worker) {
return new Promise(resolve => {
worker.port.onmessage = evt => {
resolve(evt.data);
}
});
}

async function cleanup() {
if (frame)
frame.remove();

const reg1 = await navigator.serviceWorker.getRegistration(scope1);
if (reg1)
await reg1.unregister();
const reg2 = await navigator.serviceWorker.getRegistration(scope2);
if (reg2)
await reg2.unregister();
}

// Builds the worker script URL, which encodes information about where
// to redirect to. The URL falls in sw1's scope.
//
// - |redirector| is "network" or "serviceworker". If "serviceworker", sw1 will
// respondWith() a redirect. Otherwise, it falls back to network and the server
// responds with a redirect.
// - |redirect_location| is "scope2" or "out-of-scope". If "scope2", the
// redirect ends up in sw2's scope2. Otherwise it's out of scope.
function build_worker_url(redirector, redirect_location) {
let redirect_path;
// Set path to redirect.py, a file on the server that serves
// a redirect. When sw1 sees this URL, it falls back to network.
if (redirector == 'network')
redirector_path = 'redirect.py';
// Set path to 'sw-redirect', to tell the service worker
// to respond with redirect.
else if (redirector == 'serviceworker')
redirector_path = 'sw-redirect';

let redirect_to = base_path() + 'resources/';
// Append "scope2/" to redirect_to, so the redirect falls in scope2.
// Otherwise no change is needed, as the parent "resources/" directory is
// used, and is out-of-scope.
if (redirect_location == 'scope2')
redirect_to += 'scope2/';
// Append the name of the file which serves the worker script.
redirect_to += 'worker_interception_redirect_webworker.py';

return `scope1/${redirector_path}?Redirect=${redirect_to}`
}

promise_test(async t => {
await cleanup();
const service_worker = 'resources/worker-interception-redirect-serviceworker.js';
const registration1 = await navigator.serviceWorker.register(service_worker, {scope: scope1});
await wait_for_state(t, registration1.installing, 'activated');
const registration2 = await navigator.serviceWorker.register(service_worker, {scope: scope2});
await wait_for_state(t, registration2.installing, 'activated');

promise_test(t => {
return cleanup();
}, 'cleanup global state');
}, 'initialize global state');

function worker_redirect_test(worker_url,
expected_main_resource_message,
expected_subresource_message,
description) {
promise_test(async t => {
// Create a frame to load the worker from. This way we can remove the frame
// to destroy the worker client when the test is done.
frame = await with_iframe('resources/blank.html');
t.add_cleanup(() => { frame.remove(); });

// Start the worker.
const w = new frame.contentWindow.SharedWorker(worker_url);
w.port.start();

// Expect a message from the worker indicating which service worker
// provided the response for the worker script request, if any.
const data = await get_message_from_worker(w);
assert_equals(data, expected_main_resource_message);

// The worker does a fetch() after it starts up. Expect a message from the
// worker indicating which service worker provided the response for the
// fetch(), if any.
//
// Note: for some reason, Firefox would pass all these tests if a
// postMessage ping/pong step is added before the fetch(). I.e., if the
// page does postMessage() and the worker does fetch() in response to the
// ping, the fetch() is properly intercepted. See
// https://bugzilla.mozilla.org/show_bug.cgi?id=1452528. (Chrome can't pass
// the tests either way.)
const message = get_message_from_worker(w);
const data2 = await message;
assert_equals(data2, expected_subresource_message);
}, description);
}

worker_redirect_test(
build_worker_url('network', 'scope2'),
'the shared worker script was served from network',
'fetch(): sw1 saw the fetch from the worker',
'request to sw1 scope gets network redirect to sw2 scope');

worker_redirect_test(
build_worker_url('network', 'out-scope'),
'the shared worker script was served from network',
'fetch(): sw1 saw the fetch from the worker',
'request to sw1 scope gets network redirect to out-of-scope');

worker_redirect_test(
build_worker_url('serviceworker', 'scope2'),
'sw2 saw the request for the worker script',
'fetch(): sw2 saw the fetch from the worker',
'request to sw1 scope gets service-worker redirect to sw2 scope');

worker_redirect_test(
build_worker_url('serviceworker', 'out-scope'),
'the shared worker script was served from network',
'fetch(): sw1 saw the fetch from the worker',
'request to sw1 scope gets service-worker redirect to out-of-scope');
</script>
</body>

0 comments on commit 6fe36d7

Please sign in to comment.