-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WPT: service worker: test clients.get(resultingClientId) for cross-or…
…igin. See w3c/ServiceWorker#1385 Bug: 924959 Change-Id: I5e2850b743d0702b36f1f20a84c87591c3baab19
- Loading branch information
1 parent
388ba3a
commit a7f3842
Showing
2 changed files
with
289 additions
and
0 deletions.
There are no files selected for viewing
182 changes: 182 additions & 0 deletions
182
service-workers/service-worker/clients-get-resultingClientId.https.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
<!DOCTYPE html> | ||
<meta charset="utf-8"> | ||
<title>Test clients.get(resultingClientId)</title> | ||
<script src="/resources/testharness.js"></script> | ||
<script src="/resources/testharnessreport.js"></script> | ||
<script src="/common/get-host-info.sub.js"></script> | ||
<script src="resources/test-helpers.sub.js"></script> | ||
<script> | ||
const scope = "resources/"; | ||
let worker; | ||
|
||
// Setup. Keep this as the first promise_test. | ||
promise_test(async (t) => { | ||
const registration = await service_worker_unregister_and_register( | ||
t, 'resources/get-resultingClientId-worker.js', | ||
scope); | ||
worker = registration.installing; | ||
await wait_for_state(t, worker, 'activated'); | ||
}, 'global setup'); | ||
|
||
// Sends |command| to the worker and returns a promise that resolves to its | ||
// response. There should only be one inflight command at a time. | ||
async function sendCommand(command) { | ||
const saw_message = new Promise((resolve) => { | ||
navigator.serviceWorker.onmessage = (event) => { | ||
resolve(event.data); | ||
}; | ||
}); | ||
worker.postMessage(command); | ||
return saw_message; | ||
} | ||
|
||
// Wrapper for 'startTest' command. Tells the worker a test is starting, | ||
// so it resets state and keeps itself alive until 'finishTest'. | ||
async function startTest(t) { | ||
const result = await sendCommand({command: 'startTest'}); | ||
assert_equals(result, 'ok', 'startTest'); | ||
|
||
t.add_cleanup(async () => { | ||
return finishTest(); | ||
}); | ||
} | ||
|
||
// Wrapper for 'finishTest' command. | ||
async function finishTest() { | ||
const result = await sendCommand({command: 'finishTest'}); | ||
assert_equals(result, 'ok', 'finishTest'); | ||
} | ||
|
||
// Wrapper for 'getResultingClient' command. Tells the worker to return | ||
// clients.get(event.resultingClientId) for the navigation that occurs | ||
// during this test. | ||
// | ||
// The return value describes how clients.get() settled. It also includes | ||
// |queriedId| which is the id passed to clients.get() (the resultingClientId | ||
// in this case). | ||
// | ||
// Example value: | ||
// { | ||
// queriedId: 'abc', | ||
// promiseState: fulfilled, | ||
// promiseValue: client, | ||
// client: { | ||
// id: 'abc', | ||
// url: '//example.com/client' | ||
// } | ||
// } | ||
async function getResultingClient() { | ||
return sendCommand({command: 'getResultingClient'}); | ||
} | ||
|
||
// Wrapper for 'getClient' command. Tells the worker to return | ||
// clients.get(|id|). The return value is as in the getResultingClient() | ||
// documentation. | ||
async function getClient(id) { | ||
return sendCommand({command: 'getClient', id: id}); | ||
} | ||
|
||
// Navigates to |url|. Returns the result of clients.get() on the | ||
// resultingClientId. | ||
async function navigateAndGetResultingClient(t, url) { | ||
const resultPromise = getResultingClient(); | ||
const frame = await with_iframe(url); | ||
t.add_cleanup(() => { | ||
frame.remove(); | ||
}); | ||
const result = await resultPromise; | ||
const resultingClientId = result.queriedId; | ||
|
||
// First test clients.get(event.resultingClientId) inside the fetch event. The | ||
// behavior of this is subtle due to the use of iframes and about:blank | ||
// replacement. The spec probably requires that it resolve to the original | ||
// about:blank client, and that later that client should be discarded after | ||
// load if the load was to another origin. Implementations might differ. For | ||
// now, this test just asserts that the promise resolves. See | ||
// https://github.com/w3c/ServiceWorker/issues/1385. | ||
assert_equals(result.promiseState, 'fulfilled', | ||
'get(event.resultingClientId) in the fetch event should fulfill'); | ||
|
||
// Test clients.get() on the previous resultingClientId again. By this | ||
// time the load finished, so it's more straightforward how this promise | ||
// should settle. Return this promise. | ||
return await getClient(resultingClientId); | ||
} | ||
|
||
// Test get(resultingClientId) in the basic same-origin case. | ||
promise_test(async (t) => { | ||
await startTest(t); | ||
|
||
const url = new URL('resources/empty.html', window.location); | ||
let result = await navigateAndGetResultingClient(t, url); | ||
assert_equals(result.promiseState, 'fulfilled', 'promiseState'); | ||
assert_equals(result.promiseValue, 'client', 'promiseValue'); | ||
assert_equals(result.client.url, url.href, 'client.url',); | ||
assert_equals(result.client.id, result.queriedId, 'client.id'); | ||
}, 'get(resultingClientId) for same-origin document'); | ||
|
||
// Test get(resultingClientId) when the response redirects to another origin. | ||
promise_test(async (t) => { | ||
await startTest(t); | ||
|
||
// Navigate to a URL that redirects to another origin. | ||
const base_url = new URL('.', window.location); | ||
const host_info = get_host_info(); | ||
const other_origin_url = new URL(base_url.pathname + 'resources/empty.html', | ||
host_info['HTTPS_REMOTE_ORIGIN']); | ||
const url = new URL('resources/empty.html', window.location); | ||
const pipe = `status(302)|header(Location, ${other_origin_url})`; | ||
url.searchParams.set('pipe', pipe); | ||
|
||
// The original reserved client should have been discareded on cross-origin | ||
// redirect. | ||
let result = await navigateAndGetResultingClient(t, url); | ||
assert_equals(result.promiseState, 'fulfilled', 'promiseState'); | ||
assert_equals(result.promiseValue, 'undefinedValue', 'promiseValue'); | ||
}, 'get(resultingClientId) on cross-origin redirect'); | ||
|
||
// Test get(resultingClientId) when the response is sandboxed to a unique | ||
// origin using a CSP HTTP response header. The test asserts that the promise | ||
// should resolve with undefined. | ||
// | ||
// The spec currently requires the opposite, but the test author believes this is a | ||
// mistake as cross-origin clients should not be exposed. Filed | ||
// https://github.com/w3c/ServiceWorker/issues/1385 | ||
promise_test(async (t) => { | ||
await startTest(t); | ||
|
||
// Navigate to a URL that has CSP sandboxing set in the HTTP response header. | ||
const url = new URL('resources/empty.html', window.location); | ||
const pipe = 'header(Content-Security-Policy, sandbox)'; | ||
url.searchParams.set('pipe', pipe); | ||
|
||
// The original reserved client should have been discarded upon loading | ||
// the sandboxed document. | ||
let result = await navigateAndGetResultingClient(t, url); | ||
assert_equals(result.promiseState, 'fulfilled', 'promiseState'); | ||
assert_equals(result.promiseValue, 'undefinedValue', 'promiseValue'); | ||
}, 'get(resultingClientId) for document sandboxed by CSP header'); | ||
|
||
// Test get(resultingClientId) when the response is sandboxed with | ||
// allow-same-origin. The promise should resolve with a client, | ||
// as it's same-origin. | ||
promise_test(async (t) => { | ||
await startTest(t); | ||
|
||
// Navigate to a URL that has CSP sandboxing set in the HTTP response header. | ||
const url = new URL('resources/empty.html', window.location); | ||
const pipe = 'header(Content-Security-Policy, sandbox allow-same-origin)'; | ||
url.searchParams.set('pipe', pipe); | ||
|
||
let result = await navigateAndGetResultingClient(t, url); | ||
assert_equals(result.promiseState, 'fulfilled', 'promiseState'); | ||
assert_equals(result.promiseValue, 'client', 'promiseValue'); | ||
assert_equals(result.client.url, url.href, 'client.url',); | ||
assert_equals(result.client.id, result.queriedId, 'client.id'); | ||
}, 'get(resultingClientId) for document sandboxed by CSP header with allow-same-origin'); | ||
|
||
// Cleanup. Keep this as the last promise_test. | ||
promise_test(async (t) => { | ||
return service_worker_unregister(t, scope); | ||
}, 'global cleanup'); | ||
</script> |
107 changes: 107 additions & 0 deletions
107
service-workers/service-worker/resources/get-resultingClientId-worker.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
// This worker expects a fetch event for a navigation and messages back the | ||
// result of clients.get(event.resultingClientId). | ||
|
||
// Resolves when the test finishes. | ||
let testFinishPromise; | ||
let resolveTestFinishPromise; | ||
let rejectTestFinishPromise; | ||
|
||
// Resolves to clients.get(event.resultingClientId) from the fetch event. | ||
let getPromise; | ||
let resolveGetPromise; | ||
let rejectGetPromise; | ||
|
||
let resultingClientId; | ||
|
||
function startTest() { | ||
testFinishPromise = new Promise((resolve, reject) => { | ||
resolveTestFinishPromise = resolve; | ||
rejectTestFinishPromise = reject; | ||
}); | ||
|
||
getPromise = new Promise((resolve, reject) => { | ||
resolveGetPromise = resolve; | ||
rejectGetPromise = reject; | ||
}); | ||
} | ||
|
||
async function describeGetPromiseResult(promise) { | ||
const result = {}; | ||
|
||
await promise.then( | ||
(client) => { | ||
result.promiseState = 'fulfilled'; | ||
if (client === undefined) { | ||
result.promiseValue = 'undefinedValue'; | ||
} else if (client instanceof Client) { | ||
result.promiseValue = 'client'; | ||
result.client = { | ||
id: client.id, | ||
url: client.url | ||
}; | ||
} else { | ||
result.promiseValue = 'unknown'; | ||
} | ||
}, | ||
(error) => { | ||
result.promiseState = 'rejected'; | ||
}); | ||
|
||
return result; | ||
} | ||
|
||
async function handleGetResultingClient(event) { | ||
// Note that this message can arrive before |resultingClientId| is populated. | ||
const result = await describeGetPromiseResult(getPromise); | ||
// |resultingClientId| must be populated by now. | ||
result.queriedId = resultingClientId; | ||
event.source.postMessage(result); | ||
}; | ||
|
||
async function handleGetClient(event) { | ||
const id = event.data.id; | ||
const result = await describeGetPromiseResult(self.clients.get(id)); | ||
result.queriedId = id; | ||
event.source.postMessage(result); | ||
}; | ||
|
||
self.addEventListener('message', (event) => { | ||
if (event.data.command == 'startTest') { | ||
startTest(); | ||
event.waitUntil(testFinishPromise); | ||
event.source.postMessage('ok'); | ||
return; | ||
} | ||
|
||
if (event.data.command == 'finishTest') { | ||
resolveTestFinishPromise(); | ||
event.source.postMessage('ok'); | ||
return; | ||
} | ||
|
||
if (event.data.command == 'getResultingClient') { | ||
event.waitUntil(handleGetResultingClient(event)); | ||
return; | ||
} | ||
|
||
if (event.data.command == 'getClient') { | ||
event.waitUntil(handleGetClient(event)); | ||
return; | ||
} | ||
}); | ||
|
||
async function handleFetch(event) { | ||
try { | ||
resultingClientId = event.resultingClientId; | ||
const client = await self.clients.get(resultingClientId); | ||
resolveGetPromise(client); | ||
} catch (error) { | ||
rejectGetPromise(error); | ||
} | ||
} | ||
|
||
self.addEventListener('fetch', (event) => { | ||
if (event.request.mode != 'navigate') | ||
return; | ||
event.waitUntil(handleFetch(event)); | ||
}); |