summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/service-workers/service-worker/worker-interception-redirect.https.html
blob: 8d566b9c15b10e1814dd73d3aecb1a6241ddec57 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
<!DOCTYPE html>
<title>Service Worker: controlling Worker/SharedWorker</title>
<meta name="timeout" content="long">
<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 an importScripts() and fetch(), and posts back the
//   responses, which describe where the responses where served from.

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

function get_message_from_worker(port) {
  return new Promise(resolve => {
      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');

async function worker_redirect_test(worker_request_url,
                              worker_expected_url,
                              expected_main_resource_message,
                              expected_import_scripts_message,
                              expected_fetch_message,
                              description) {
  for (const workerType of ['DedicatedWorker', 'SharedWorker']) {
    for (const type of ['classic', 'module']) {
      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.
        let w;
        let port;
        if (workerType === 'DedicatedWorker') {
          w = new frame.contentWindow.Worker(worker_request_url, {type});
          port = w;
        } else {
          w = new frame.contentWindow.SharedWorker(worker_request_url, {type});
          port = w.port;
          w.port.start();
        }
        w.onerror = t.unreached_func('Worker error');

        // 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(port);

        // The worker does an importScripts(). Expect a message from the worker
        // indicating which service worker provided the response for the
        // importScripts(), if any.
        const import_scripts_message = await get_message_from_worker(port);
        test(() => {
          if (type === 'classic') {
            assert_equals(import_scripts_message,
                          expected_import_scripts_message);
          } else {
            assert_equals(import_scripts_message, 'importScripts failed');
          }
        }, `${description} (${type} ${workerType}, importScripts())`);

        // The worker does a fetch(). Expect a message from the worker
        // indicating which service worker provided the response for the
        // fetch(), if any.
        const fetch_message = await get_message_from_worker(port);
        test(() => {
          assert_equals(fetch_message, expected_fetch_message);
        }, `${description} (${type} ${workerType}, fetch())`);

        // Expect a message from the worker indicating |self.location|.
        const worker_actual_url = await get_message_from_worker(port);
        test(() => {
          assert_equals(
            worker_actual_url,
            (new URL(worker_expected_url, location.href)).toString(),
            'location.href');
        }, `${description} (${type} ${workerType}, location.href)`);

        assert_equals(data, expected_main_resource_message);

      }, `${description} (${type} ${workerType})`);
    }
  }
}

// request to sw1 scope gets network redirect to sw2 scope
worker_redirect_test(
    build_worker_url('network', 'scope2'),
    'resources/scope2/worker_interception_redirect_webworker.py',
    'the worker script was served from network',
    'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/scope2/import-scripts-echo.py',
    'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/scope2/simple.txt',
    'Case #1: network scope1->scope2');

// request to sw1 scope gets network redirect to out-of-scope
worker_redirect_test(
    build_worker_url('network', 'out-scope'),
    'resources/worker_interception_redirect_webworker.py',
    'the worker script was served from network',
    'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/import-scripts-echo.py',
    'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/simple.txt',
    'Case #2: network scope1->out-scope');

// request to sw1 scope gets service-worker redirect to sw2 scope
worker_redirect_test(
    build_worker_url('serviceworker', 'scope2'),
    'resources/subdir/worker_interception_redirect_webworker.py?greeting=sw2%20saw%20the%20request%20for%20the%20worker%20script',
    'sw2 saw the request for the worker script',
    'sw2 saw importScripts from the worker: /service-workers/service-worker/resources/subdir/import-scripts-echo.py',
    'fetch(): sw2 saw the fetch from the worker: /service-workers/service-worker/resources/subdir/simple.txt',
    'Case #3: sw scope1->scope2');

// request to sw1 scope gets service-worker redirect to out-of-scope
worker_redirect_test(
    build_worker_url('serviceworker', 'out-scope'),
    'resources/worker_interception_redirect_webworker.py',
    'the worker script was served from network',
    'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/import-scripts-echo.py',
    'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/simple.txt',
    'Case #4: sw scope1->out-scope');
</script>
</body>