diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /testing/web-platform/tests/webtransport | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/webtransport')
26 files changed, 1715 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webtransport/META.yml b/testing/web-platform/tests/webtransport/META.yml new file mode 100644 index 0000000000..3499931112 --- /dev/null +++ b/testing/web-platform/tests/webtransport/META.yml @@ -0,0 +1,5 @@ +spec: https://w3c.github.io/webtransport/ +suggested_reviewers: + - bashi + - nidhijaju + - yutakahirano diff --git a/testing/web-platform/tests/webtransport/README.md b/testing/web-platform/tests/webtransport/README.md new file mode 100644 index 0000000000..543d876e01 --- /dev/null +++ b/testing/web-platform/tests/webtransport/README.md @@ -0,0 +1,14 @@ +# Notes about WebTransport WPTs + +### Running the WebTransport WPTs using the test server +Although these tests do not currently run on the Chromium CI, they can still be +run using the WebTransport WPT server manually. + +Please refer to the following document for detailed instructions: +[Running WPT WebTransport test server](https://docs.google.com/document/d/1OBoZTcC9vDoLTgv_5WUznRFrmwXP0Gprj7V9oOzH9cU/edit?usp=sharing) + + +### Server Handlers +The python server handlers are stored under `handlers/` and are written using the +PEP8 style. +For details, please see the [style guide](https://www.python.org/dev/peps/pep-0008/). diff --git a/testing/web-platform/tests/webtransport/bidirectional-cancel-crash.https.html b/testing/web-platform/tests/webtransport/bidirectional-cancel-crash.https.html new file mode 100644 index 0000000000..6919c90aa8 --- /dev/null +++ b/testing/web-platform/tests/webtransport/bidirectional-cancel-crash.https.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<html class="test-wait"> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/webtransport-test-helpers.sub.js"></script> +<script type="module"> + const WT_CODE = 127; + const HTTP_CODE = webtransport_code_to_http_code(WT_CODE); + const wt = new WebTransport( + webtransport_url(`abort-stream-from-server.py?code=${HTTP_CODE}`)); + await wt.ready; + + const bidi = await wt.createBidirectionalStream(); + const writer = bidi.writable.getWriter(); + + const reader = bidi.readable.getReader(); + reader.read(); + + // Write something, to make the stream visible to the server side. + await writer.write(new Uint8Array([64])); + + const e = await reader.closed.catch(e => e); + document.documentElement.classList.remove("test-wait"); +</script> diff --git a/testing/web-platform/tests/webtransport/close.https.any.js b/testing/web-platform/tests/webtransport/close.https.any.js new file mode 100644 index 0000000000..de675036f3 --- /dev/null +++ b/testing/web-platform/tests/webtransport/close.https.any.js @@ -0,0 +1,153 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/webtransport-test-helpers.sub.js +// META: script=/common/utils.js +// META: timeout=long + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + wt.close(); + + const close_info = await wt.closed; + + assert_equals(close_info.closeCode, 0, 'code'); + assert_equals(close_info.reason, '', 'reason'); + + await wait(10); + const data = await query(id); + + assert_own_property(data, 'session-close-info'); + const info = data['session-close-info'] + + assert_false(info.abruptly, 'abruptly'); + assert_equals(info.close_info.code, 0, 'code'); + assert_equals(info.close_info.reason, '', 'reason'); +}, 'close'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + wt.close({closeCode: 99, reason: 'reason X'}); + + const close_info = await wt.closed; + + assert_equals(close_info.closeCode, 99, 'code'); + assert_equals(close_info.reason, 'reason X', 'reason'); + + await wait(10); + const data = await query(id); + + assert_own_property(data, 'session-close-info'); + const info = data['session-close-info'] + + assert_false(info.abruptly, 'abruptly'); + assert_equals(info.close_info.code, 99, 'code'); + assert_equals(info.close_info.reason, 'reason X', 'reason'); +}, 'close with code and reason'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + const reason = 'あいうえお'.repeat(1000); + + wt.close({closeCode: 11, reason}); + + const close_info = await wt.closed; + + assert_equals(close_info.closeCode, 11, 'code'); + // `close_info.reason` should report the original, non-truncated reason as + // step 9 of https://w3c.github.io/webtransport/#dom-webtransport-close + // uses the original `closeInfo` to perform `Cleanup`. + assert_equals(close_info.reason, reason, 'reason'); + + await wait(10); + const data = await query(id); + + assert_own_property(data, 'session-close-info'); + const info = data['session-close-info'] + + // Server should have received truncated reason as step 6 of + // https://w3c.github.io/webtransport/#dom-webtransport-close specifies. + const expected_reason = + new TextDecoder().decode( + new TextEncoder().encode(reason).slice(0, 1024)).replaceAll('\ufffd', ''); + assert_false(info.abruptly, 'abruptly'); + assert_equals(info.close_info.code, 11, 'code'); + assert_equals(info.close_info.reason, expected_reason, 'reason'); +}, 'close with code and long reason'); + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('server-close.py')); + + const close_info = await wt.closed; + assert_equals(close_info.closeCode, 0, 'code'); + assert_equals(close_info.reason, '', 'reason'); +}, 'server initiated closure without code and reason'); + +promise_test(async t => { + const code = 32; + const reason = 'abc'; + const wt = new WebTransport( + webtransport_url(`server-close.py?code=${code}&reason=${reason}`)); + add_completion_callback(() => wt.close()); + + const close_info = await wt.closed; + assert_equals(close_info.closeCode, code, 'code'); + assert_equals(close_info.reason, reason, 'reason'); +}, 'server initiated closure with code and reason'); + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('server-connection-close.py')); + add_completion_callback(() => wt.close()); + + const streams_reader = wt.incomingBidirectionalStreams.getReader(); + const { value: bidi } = await streams_reader.read(); + const writer = bidi.writable.getWriter(); + const reader = bidi.readable.getReader(); + try { + writer.write(new Uint8Array([65])); + } catch (e) { + } + + // Sadly we cannot use promise_rejects_dom as the error constructor is + // WebTransportError rather than DOMException. + // We get a possible error, and then make sure wt.closed is rejected with it. + const e = await wt.closed.catch(e => e); + await promise_rejects_exactly(t, e, wt.closed, 'wt.closed'); + await promise_rejects_exactly(t, e, writer.closed, 'writer.closed'); + await promise_rejects_exactly(t, e, reader.closed, 'reader.closed'); + assert_true(e instanceof WebTransportError); + assert_equals(e.source, 'session', 'source'); + assert_equals(e.streamErrorCode, null, 'streamErrorCode'); +}, 'server initiated connection closure'); + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('echo.py')); + const stream = await wt.createUnidirectionalStream(); + await wt.ready; +}, 'opening unidirectional stream before ready'); + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('echo.py')); + const stream = await wt.createBidirectionalStream(); + await wt.ready; +}, 'opening bidirectional stream before ready'); + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('server-close.py')); + promise_rejects_dom(t, "InvalidStateError", wt.createUnidirectionalStream()); +}, 'server initiated closure while opening unidirectional stream before ready'); + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('server-close.py')); + promise_rejects_dom(t, "InvalidStateError", wt.createBidirectionalStream()); +}, 'server initiated closure while opening bidirectional stream before ready'); diff --git a/testing/web-platform/tests/webtransport/connect.https.any.js b/testing/web-platform/tests/webtransport/connect.https.any.js new file mode 100644 index 0000000000..6bd18ae8de --- /dev/null +++ b/testing/web-platform/tests/webtransport/connect.https.any.js @@ -0,0 +1,89 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/webtransport-test-helpers.sub.js +// META: timeout=long + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('custom-response.py?:status=200')); + await wt.ready; +}, 'WebTransport session is established with status code 200'); + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('custom-response.py?:status=204')); + await wt.ready; +}, 'WebTransport session is established with status code 204'); + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('custom-response.py?:status=301')); + // Sadly we cannot use promise_rejects_dom as the error constructor is + // WebTransportError rather than DOMException. Ditto below. + // We get a possible error, and then make sure wt.closed is rejected with it. + const e = await wt.ready.catch(e => e); + await promise_rejects_exactly(t, e, wt.closed, 'closed promise should be rejected'); + await promise_rejects_exactly(t, e, wt.ready, 'ready promise shoud be rejected'); + assert_true(e instanceof WebTransportError); + assert_equals(e.source, 'session', 'source'); + assert_equals(e.streamErrorCode, null, 'streamErrorCode'); +}, 'WebTransport session establishment fails with status code 301'); + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('custom-response.py?:status=401')); + const e = await wt.ready.catch(e => e); + await promise_rejects_exactly(t, e, wt.closed, 'closed promise should be rejected'); + await promise_rejects_exactly(t, e, wt.ready, 'ready promise shoud be rejected'); + assert_true(e instanceof WebTransportError); + assert_equals(e.source, 'session', 'source'); + assert_equals(e.streamErrorCode, null, 'streamErrorCode'); +}, 'WebTransport session establishment with status code 401'); + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('custom-response.py?:status=404')); + const e = await wt.ready.catch(e => e); + await promise_rejects_exactly(t, e, wt.closed, 'closed promise should be rejected'); + await promise_rejects_exactly(t, e, wt.ready, 'ready promise shoud be rejected'); + assert_true(e instanceof WebTransportError); + assert_equals(e.source, 'session', 'source'); + assert_equals(e.streamErrorCode, null, 'streamErrorCode'); +}, 'WebTransport session establishment fails with status code 404'); + +promise_test(async t => { + // Create WebTransport session. + const wt = new WebTransport(webtransport_url('echo-request-headers.py')); + await wt.ready; + + // Read incoming unidirectional stream for echoed request headers. + const streams = await wt.incomingUnidirectionalStreams; + + const stream_reader = streams.getReader(); + const { value: recv_stream } = await stream_reader.read(); + stream_reader.releaseLock(); + + const request_headers = await read_stream_as_json(recv_stream); + + // Check the standard request headers. + check_and_remove_standard_headers(request_headers); +}, 'Echo back request headers'); + +promise_test(async t => { + // Create WebTransport session, and attach "Set-Cookie: foo=bar" to the response of + // the handshake. + const encodedSetCookie = encodeURIComponent('foo=bar'); + let wt = new WebTransport(webtransport_url('custom-response.py?set-cookie=' + encodedSetCookie)); + await wt.ready; + + wt = new WebTransport(webtransport_url('echo-request-headers.py')); + await wt.ready; + + // Read incoming unidirectional stream for echoed request headers. + const streams = await wt.incomingUnidirectionalStreams; + + const stream_reader = streams.getReader(); + const { value: recv_stream } = await stream_reader.read(); + stream_reader.releaseLock(); + + const request_headers = await read_stream_as_json(recv_stream); + + // Check cookie header is not echoed back. + check_and_remove_standard_headers(request_headers); + assert_equals(request_headers['cookie'], undefined); +}, 'Cookie header is not echoed back'); diff --git a/testing/web-platform/tests/webtransport/constructor.https.any.js b/testing/web-platform/tests/webtransport/constructor.https.any.js new file mode 100644 index 0000000000..0f39c4993b --- /dev/null +++ b/testing/web-platform/tests/webtransport/constructor.https.any.js @@ -0,0 +1,57 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/webtransport-test-helpers.sub.js +// META: script=/common/utils.js + +const BAD_URLS = [ + null, + '', + 'no-scheme', + 'http://example.com/' /* scheme is wrong */, + 'quic-transport://example.com/' /* scheme is wrong */, + 'https:///' /* no host specified */, + 'https://example.com/#failing' /* has fragment */, + `https://${HOST}:999999/` /* invalid port */, +]; + +for (const url of BAD_URLS) { + test(() => { + assert_throws_dom('SyntaxError', () => new WebTransport(url), + 'constructor should throw'); + }, `WebTransport constructor should reject URL '${url}'`); +} + +const OPTIONS = [ + { allowPooling: true }, + { requireUnreliable: true }, + { allowPooling: true, requireUnreliable: true }, + { congestionControl: "default" }, + { congestionControl: "throughput" }, + { congestionControl: "low-latency" }, + { allowPooling: true, requireUnreliable: true, congestionControl: "low-latency" }, + // XXX Need to test serverCertificateHashes +]; + +for (const options of OPTIONS) { + promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`), options ); + await wt.ready; + wt.close(); + }, "WebTransport constructor should allow options " + JSON.stringify(options)); +} + +promise_test(async t => { + const wt = new WebTransport(`https://${HOST}:0/`); + + // Sadly we cannot use promise_rejects_dom as the error constructor is + // WebTransportError rather than DOMException. + // We get a possible error, and then make sure wt.ready is rejected with it. + const e = await wt.ready.catch(e => e); + + await promise_rejects_exactly(t, e, wt.ready, 'ready should be rejected'); + await promise_rejects_exactly(t, e, wt.closed, 'closed should be rejected'); + assert_true(e instanceof WebTransportError); + assert_equals(e.source, 'session', 'source'); + assert_equals(e.streamErrorCode, null, 'streamErrorCode'); +}, 'Connection to port 0 should fail'); diff --git a/testing/web-platform/tests/webtransport/csp-fail.https.window.js b/testing/web-platform/tests/webtransport/csp-fail.https.window.js new file mode 100644 index 0000000000..f2e49fad99 --- /dev/null +++ b/testing/web-platform/tests/webtransport/csp-fail.https.window.js @@ -0,0 +1,27 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/webtransport-test-helpers.sub.js + +function set_csp(destination) { + let meta = document.createElement('meta'); + meta.httpEquiv = 'Content-Security-Policy'; + meta.content = `connect-src ${destination}`; + return meta; +} + +promise_test(async t => { + let meta = set_csp("'none'"); + document.head.appendChild(meta); + + let wt = new WebTransport(webtransport_url('custom-response.py?:status=200')); + + // Sadly we cannot use promise_rejects_dom as the error constructor is + // WebTransportError rather than DOMException. + const e = await wt.ready.catch(e => e); + await promise_rejects_exactly(t, e, wt.ready, 'ready promise should be rejected'); + await promise_rejects_exactly(t, e, wt.closed, 'closed promise should be rejected'); + assert_equals(e.name, 'WebTransportError', 'WebTransportError'); + assert_true(e instanceof WebTransportError); + assert_equals(e.source, 'session', 'source'); + assert_equals(e.streamErrorCode, null, 'streamErrorCode'); +}, 'WebTransport connection should fail when CSP connect-src is set to none and reject the promises'); diff --git a/testing/web-platform/tests/webtransport/csp-pass.https.window.js b/testing/web-platform/tests/webtransport/csp-pass.https.window.js new file mode 100644 index 0000000000..3c315d078f --- /dev/null +++ b/testing/web-platform/tests/webtransport/csp-pass.https.window.js @@ -0,0 +1,18 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/webtransport-test-helpers.sub.js + +function set_csp(destination) { + let meta = document.createElement('meta'); + meta.httpEquiv = 'Content-Security-Policy'; + meta.content = `connect-src ${destination}`; + return meta; +} + +promise_test(async t => { + let meta = set_csp(`${BASE}`); + document.head.appendChild(meta); + + let wt = new WebTransport(webtransport_url('custom-response.py?:status=200')); + await wt.ready; +}, 'WebTransport connection should succeed when CSP connect-src destination is set to the page'); diff --git a/testing/web-platform/tests/webtransport/datagram-cancel-crash.https.window.js b/testing/web-platform/tests/webtransport/datagram-cancel-crash.https.window.js new file mode 100644 index 0000000000..fae613e213 --- /dev/null +++ b/testing/web-platform/tests/webtransport/datagram-cancel-crash.https.window.js @@ -0,0 +1,12 @@ +// This test reproduces Chromium issue https://crbug.com/1292387. If it doesn't +// crash then the test passed. + +test(() => { + const iframeTag = document.createElement('iframe'); + document.body.appendChild(iframeTag); + const wt = new iframeTag.contentWindow.WebTransport('https://example.com/'); + iframeTag.remove(); + const datagrams = wt.datagrams; + const reader = datagrams.readable; + reader.cancel(); +}, 'call cancel() on stream in destroyed realm'); diff --git a/testing/web-platform/tests/webtransport/datagrams.https.any.js b/testing/web-platform/tests/webtransport/datagrams.https.any.js new file mode 100644 index 0000000000..d5b8e3bbda --- /dev/null +++ b/testing/web-platform/tests/webtransport/datagrams.https.any.js @@ -0,0 +1,377 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/webtransport-test-helpers.sub.js + +// Write datagrams until the producer receives the AbortSignal. +async function write_datagrams(writer, signal) { + const encoder = new TextEncoder(); + let counter = 0; + const sentTokens = []; + const aborted = new Promise((resolve) => { + signal.addEventListener('abort', resolve); + }); + while (true) { + await Promise.race([writer.ready, aborted]); + if (signal.aborted) { + break; + } + var token = counter.toString(); + sentTokens.push(token); + writer.write(encoder.encode(token)); + counter++; + } + return sentTokens; +} + +// Write N datagrams without waiting, then wait for them +async function write_N_datagrams(writer, n) { + const encoder = new TextEncoder(); + const sentTokens = []; + const promises = []; + while (sentTokens.length < n) { + const token = sentTokens.length.toString(); + sentTokens.push(token); + promises.push(writer.write(encoder.encode(token))); + } + await Promise.all(promises); + return sentTokens; +} + +// Read datagrams until the consumer has received enough i.e. N datagrams. Call +// abort() after reading. +async function read_datagrams(reader, controller, N) { + const decoder = new TextDecoder(); + const receivedTokens = []; + while (receivedTokens.length < N) { + const { value: token, done } = await reader.read(); + assert_false(done); + receivedTokens.push(decoder.decode(token)); + } + controller.abort(); + return receivedTokens; +} + +// Write numbers until the producer receives the AbortSignal. +async function write_numbers(writer, signal) { + let counter = 0; + const sentNumbers = []; + const aborted = + new Promise((resolve) => signal.addEventListener('abort', resolve)); + // Counter should be less than 256 because reader stores numbers in Uint8Array. + while (counter < 256) { + await Promise.race([writer.ready, aborted]) + if (signal.aborted) { + break; + } + sentNumbers.push(counter); + chunk = new Uint8Array(1); + chunk[0] = counter; + writer.write(chunk); + counter++; + } + return sentNumbers; +} + +// Write large datagrams of size 10 until the producer receives the AbortSignal. +async function write_large_datagrams(writer, signal) { + const aborted = new Promise((resolve) => { + signal.addEventListener('abort', resolve); + }); + while (true) { + await Promise.race([writer.ready, aborted]); + if (signal.aborted) { + break; + } + writer.write(new Uint8Array(10)); + } +} + +// Read datagrams with BYOB reader until the consumer has received enough i.e. N +// datagrams. Call abort() after reading. +async function read_numbers_byob(reader, controller, N) { + let buffer = new ArrayBuffer(N); + buffer = await readInto(reader, buffer); + controller.abort(); + return Array.from(new Uint8Array(buffer)); +} + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + const writer = wt.datagrams.writable.getWriter(); + const reader = wt.datagrams.readable.getReader(); + + const controller = new AbortController(); + const signal = controller.signal; + + // Write and read datagrams. + const N = 5; + const [sentTokens, receivedTokens] = await Promise.all([ + write_datagrams(writer, signal), + read_datagrams(reader, controller, N) + ]); + + // Check receivedTokens is a subset of sentTokens. + const subset = receivedTokens.every(token => sentTokens.includes(token)); + assert_true(subset); +}, 'Datagrams are echoed successfully'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + const writer = wt.datagrams.writable.getWriter(); + const reader = wt.datagrams.readable.getReader({ mode: 'byob' }); + + const controller = new AbortController(); + const signal = controller.signal; + + // Write and read datagrams. + // Numbers are less than 256, consider N to be a small number. + const N = 5; + const [sentNumbers, receiveNumbers] = await Promise.all([ + write_numbers(writer, signal), + read_numbers_byob(reader, controller, N) + ]); + + // No duplicated numbers received. + assert_equals((new Set(receiveNumbers)).size, N); + + // Check receiveNumbers is a subset of sentNumbers. + const subset = receiveNumbers.every(token => sentNumbers.includes(token)); + assert_true(subset); +}, 'Successfully reading datagrams with BYOB reader.'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + const writer = wt.datagrams.writable.getWriter(); + const reader = wt.datagrams.readable.getReader({ mode: 'byob' }); + + const controller = new AbortController(); + const signal = controller.signal; + + // Write datagrams of size 10, but only 1 byte buffer is provided for BYOB + // reader. To avoid splitting a datagram, stream will be errored. + const buffer = new ArrayBuffer(1); + const [error, _] = await Promise.all([ + reader.read(new Uint8Array(buffer)).catch(e => { + controller.abort(); + return e; + }), + write_large_datagrams(writer, signal) + ]); + assert_equals(error.name, 'RangeError'); +}, 'Reading datagrams with insufficient buffer should be rejected.'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo_datagram_length.py')); + await wt.ready; + + const writer = wt.datagrams.writable.getWriter(); + const reader = wt.datagrams.readable.getReader(); + + // Write and read max-size datagram. + const maxDatagramSize = wt.datagrams.maxDatagramSize; + await writer.write(new Uint8Array(maxDatagramSize)); + + // the server should echo the datagram length encoded in JSON + const { value: token, done } = await reader.read(); + assert_false(done); + + const decoder = new TextDecoder(); + const datagramStr = decoder.decode(token); + const jsonObject = JSON.parse(datagramStr); + assert_equals(jsonObject['length'], maxDatagramSize); +}, 'Transfer max-size datagram'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + const writer = wt.datagrams.writable.getWriter(); + const reader = wt.datagrams.readable.getReader(); + + // Write and read max-size datagram. + await writer.write(new Uint8Array(wt.datagrams.maxDatagramSize+1)); + // This should resolve with no datagram sent, which is hard to test for. + // Wait for incoming datagrams to arrive, and if they do, fail. + const result = await Promise.race([reader.read(), wait(500)]); + assert_equals(result, undefined); +}, 'Fail to transfer max-size+1 datagram'); + +promise_test(async t => { + // Make a WebTransport connection, but session is not necessarily established. + const wt = new WebTransport(webtransport_url('echo.py')); + + const writer = wt.datagrams.writable.getWriter(); + const reader = wt.datagrams.readable.getReader(); + + const controller = new AbortController(); + const signal = controller.signal; + + // Write and read datagrams. + const N = 5; + wt.datagrams.outgoingHighWaterMark = N; + const [sentTokens, receivedTokens] = await Promise.all([ + write_N_datagrams(writer, N), + read_datagrams(reader, controller, N) + ]); + + // Check receivedTokens is a subset of sentTokens. + const subset = receivedTokens.every(token => sentTokens.includes(token)); + assert_true(subset); + + // Make sure WebTransport session is established. + await wt.ready; +}, 'Sending and receiving datagrams is ready to use before session is established'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + const N = 5; + wt.datagrams.outgoingHighWaterMark = N; + + const writer = wt.datagrams.writable.getWriter(); + const encoder = new TextEncoder(); + + // Write N-1 datagrams. + let counter; + for (counter = 0; counter < N-1; counter++) { + var datagram = counter.toString(); + let resolved = false; + writer.write(encoder.encode(datagram)); + + // Check writer.ready resolves immediately. + writer.ready.then(() => resolved = true); + // TODO(nidhijaju): The number of `await Promise.resolve()` calls is + // implementation dependent, so we should not have this as the final + // solution. + for (let i = 0; i < 10; i++) { + await Promise.resolve(); + } + assert_true(resolved); + } + + // Write one more datagram. + resolved = false; + const last_datagram = counter.toString(); + writer.write(encoder.encode(last_datagram)); + + // Check writer.ready does not resolve immediately. + writer.ready.then(() => resolved = true); + for (let i = 0; i < 10; i++) { + await Promise.resolve(); + } + assert_false(resolved); + + // Make sure writer.ready is resolved eventually. + await writer.ready; +}, 'Datagram\'s outgoingHighWaterMark correctly regulates written datagrams'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + const N = 5; + wt.datagrams.incomingHighWaterMark = N; + + const writer = wt.datagrams.writable.getWriter(); + const encoder = new TextEncoder(); + + // Write 10*N datagrams. + let counter; + for (counter = 0; counter < 10*N; counter++) { + var datagram = counter.toString(); + writer.write(encoder.encode(datagram)); + await writer.ready; + } + + // Wait for incoming datagrams to arrive. + wait(500); + + const reader = wt.datagrams.readable.getReader(); + + // Read all of the immediately available datagrams. + let receivedDatagrams = 0; + while (true) { + let resolved = false; + reader.read().then(() => resolved = true); + // TODO(nidhijaju): Find a better solution instead of just having numerous + // `await Promise.resolve()` calls. + for (let i = 0; i < 10; i++) { + await Promise.resolve(); + } + if (!resolved) { + break; + } + receivedDatagrams++; + } + + // Check that the receivedDatagrams is less than or equal to the + // incomingHighWaterMark. + assert_less_than_equal(receivedDatagrams, N); +}, 'Datagrams read is less than or equal to the incomingHighWaterMark'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + assert_equals(wt.datagrams.incomingMaxAge, Infinity); + assert_equals(wt.datagrams.outgoingMaxAge, Infinity); + + wt.datagrams.incomingMaxAge = 5; + assert_equals(wt.datagrams.incomingMaxAge, 5); + wt.datagrams.outgoingMaxAge = 5; + assert_equals(wt.datagrams.outgoingMaxAge, 5); + + assert_throws_js(RangeError, () => { wt.datagrams.incomingMaxAge = -1; }); + assert_throws_js(RangeError, () => { wt.datagrams.outgoingMaxAge = -1; }); + assert_throws_js(RangeError, () => { wt.datagrams.incomingMaxAge = NaN; }); + assert_throws_js(RangeError, () => { wt.datagrams.outgoingMaxAge = NaN; }); + + wt.datagrams.incomingMaxAge = 0; + assert_equals(wt.datagrams.incomingMaxAge, Infinity); + wt.datagrams.outgoingMaxAge = 0; + assert_equals(wt.datagrams.outgoingMaxAge, Infinity); +}, 'Datagram MaxAge getters/setters work correctly'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + // Initial values are implementation-defined + assert_greater_than_equal(wt.datagrams.incomingHighWaterMark, 1); + assert_greater_than_equal(wt.datagrams.outgoingHighWaterMark, 1); + + wt.datagrams.incomingHighWaterMark = 5; + assert_equals(wt.datagrams.incomingHighWaterMark, 5); + wt.datagrams.outgoingHighWaterMark = 5; + assert_equals(wt.datagrams.outgoingHighWaterMark, 5); + + assert_throws_js(RangeError, () => { wt.datagrams.incomingHighWaterMark = -1; }); + assert_throws_js(RangeError, () => { wt.datagrams.outgoingHighWaterMark = -1; }); + assert_throws_js(RangeError, () => { wt.datagrams.incomingHighWaterMark = NaN; }); + assert_throws_js(RangeError, () => { wt.datagrams.outgoingHighWaterMark = NaN; }); + + wt.datagrams.incomingHighWaterMark = 0.5; + assert_equals(wt.datagrams.incomingHighWaterMark, 1); + wt.datagrams.outgoingHighWaterMark = 0.5; + assert_equals(wt.datagrams.outgoingHighWaterMark, 1); + wt.datagrams.incomingHighWaterMark = 0; + assert_equals(wt.datagrams.incomingHighWaterMark, 1); + wt.datagrams.outgoingHighWaterMark = 0; + assert_equals(wt.datagrams.outgoingHighWaterMark, 1); +}, 'Datagram HighWaterMark getters/setters work correctly'); diff --git a/testing/web-platform/tests/webtransport/echo-large-bidirectional-streams.https.html b/testing/web-platform/tests/webtransport/echo-large-bidirectional-streams.https.html new file mode 100644 index 0000000000..daa7c1b9b3 --- /dev/null +++ b/testing/web-platform/tests/webtransport/echo-large-bidirectional-streams.https.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/webtransport-test-helpers.sub.js"></script> +<script> +// A test that aims to reproduce https://crbug.com/1369030 -- note that since +// the bug in question is a race condition, this test will probably be flaky if +// this is actually broken. +promise_test(async t => { + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + const numBytes = 1024 * 1024; + const numStreams = 5; + for (let i = 0; i < numStreams; i++) { + const stream = await wt.createBidirectionalStream(); + const writer = stream.writable.getWriter(); + await writer.write(new Uint8Array(numBytes)); + await writer.close(); + const response = await (new Response(stream.readable).arrayBuffer()); + assert_equals(response.byteLength, numBytes); + } +}, 'Ensure large bidirectional streams does not cause race condition'); + +</script> diff --git a/testing/web-platform/tests/webtransport/handlers/abort-stream-from-server.py b/testing/web-platform/tests/webtransport/handlers/abort-stream-from-server.py new file mode 100644 index 0000000000..1d92b705e6 --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/abort-stream-from-server.py @@ -0,0 +1,27 @@ +from typing import Optional +from urllib.parse import urlsplit, parse_qsl + + +def session_established(session): + path: Optional[bytes] = None + for key, value in session.request_headers: + if key == b':path': + path = value + assert path is not None + qs = dict(parse_qsl(urlsplit(path).query)) + code = qs[b'code'] + if code is None: + raise Exception('code is missing, path = {}'.format(path)) + session.dict_for_handlers['code'] = int(code) + + +def stream_data_received(session, + stream_id: int, + data: bytes, + stream_ended: bool): + code: int = session.dict_for_handlers['code'] + if session.stream_is_unidirectional(stream_id): + session.stop_stream(stream_id, code) + else: + session.stop_stream(stream_id, code) + session.reset_stream(stream_id, code)
\ No newline at end of file diff --git a/testing/web-platform/tests/webtransport/handlers/client-close.py b/testing/web-platform/tests/webtransport/handlers/client-close.py new file mode 100644 index 0000000000..01fb14b134 --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/client-close.py @@ -0,0 +1,59 @@ +from typing import Optional, Tuple +from urllib.parse import urlsplit, parse_qsl + + +def session_established(session): + path: Optional[bytes] = None + for key, value in session.request_headers: + if key == b':path': + path = value + assert path is not None + qs = dict(parse_qsl(urlsplit(path).query)) + token = qs[b'token'] + if token is None: + raise Exception('token is missing, path = {}'.format(path)) + session.dict_for_handlers['token'] = token + session.create_bidirectional_stream() + + +def stream_reset(session, stream_id: int, error_code: int) -> None: + token = session.dict_for_handlers['token'] + data = session.stash.take(key=token) or {} + + data['stream-close-info'] = { + 'source': 'reset', + 'code': error_code + } + session.stash.put(key=token, value=data) + + +def stream_data_received(session, + stream_id: int, + data: bytes, + stream_ended: bool): + if stream_ended: + token = session.dict_for_handlers['token'] + stashed_data = session.stash.take(key=token) or {} + stashed_data['stream-close-info'] = { + 'source': 'FIN', + } + session.stash.put(key=token, value=stashed_data) + + +def session_closed( + session, close_info: Optional[Tuple[int, bytes]], abruptly: bool) -> None: + token = session.dict_for_handlers['token'] + data = session.stash.take(key=token) or {} + + decoded_close_info: Optional[Dict[str, Any]] = None + if close_info: + decoded_close_info = { + 'code': close_info[0], + 'reason': close_info[1].decode() + } + + data['session-close-info'] = { + 'abruptly': abruptly, + 'close_info': decoded_close_info + } + session.stash.put(key=token, value=data) diff --git a/testing/web-platform/tests/webtransport/handlers/custom-response.py b/testing/web-platform/tests/webtransport/handlers/custom-response.py new file mode 100644 index 0000000000..05e4c4ba36 --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/custom-response.py @@ -0,0 +1,14 @@ +from urllib.parse import urlsplit, parse_qsl + + +def connect_received(request_headers, response_headers): + for data in request_headers: + if data[0] == b':path': + path = data[1].decode('utf-8') + + qs = dict(parse_qsl(urlsplit(path).query)) + for key, value in qs.items(): + response_headers.append((key.encode('utf-8'), value.encode('utf-8'))) + + break + return diff --git a/testing/web-platform/tests/webtransport/handlers/echo-request-headers.py b/testing/web-platform/tests/webtransport/handlers/echo-request-headers.py new file mode 100644 index 0000000000..122d6f0601 --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/echo-request-headers.py @@ -0,0 +1,11 @@ +import json + + +def session_established(session): + headers = {} + for name, value in session.request_headers: + headers[name.decode('utf-8')] = value.decode('utf-8') + + stream_id = session.create_unidirectional_stream() + data = json.dumps(headers).encode('utf-8') + session.send_stream_data(stream_id, data, end_stream=True) diff --git a/testing/web-platform/tests/webtransport/handlers/echo.py b/testing/web-platform/tests/webtransport/handlers/echo.py new file mode 100644 index 0000000000..4347d4aae3 --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/echo.py @@ -0,0 +1,33 @@ +streams_dict = {} + + +def session_established(session): + # When a WebTransport session is established, a bidirectional stream is + # created by the server, which is used to echo back stream data from the + # client. + session.create_bidirectional_stream() + + +def stream_data_received(session, + stream_id: int, + data: bytes, + stream_ended: bool): + # If a stream is unidirectional, create a new unidirectional stream and echo + # back the data on that stream. + if session.stream_is_unidirectional(stream_id): + if (session.session_id, stream_id) not in streams_dict.keys(): + new_stream_id = session.create_unidirectional_stream() + streams_dict[(session.session_id, stream_id)] = new_stream_id + session.send_stream_data(streams_dict[(session.session_id, stream_id)], + data, + end_stream=stream_ended) + if (stream_ended): + del streams_dict[(session.session_id, stream_id)] + return + # Otherwise (e.g. if the stream is bidirectional), echo back the data on the + # same stream. + session.send_stream_data(stream_id, data, end_stream=stream_ended) + + +def datagram_received(session, data: bytes): + session.send_datagram(data) diff --git a/testing/web-platform/tests/webtransport/handlers/echo_datagram_length.py b/testing/web-platform/tests/webtransport/handlers/echo_datagram_length.py new file mode 100644 index 0000000000..f0610b085f --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/echo_datagram_length.py @@ -0,0 +1,8 @@ +import json + +def datagram_received(session, data: bytes): + # encode the received length into a JSON string and send back + data_len = len(data) + out_datagram_json = json.dumps({'length': data_len}) + out_data = out_datagram_json.encode() + session.send_datagram(out_data) diff --git a/testing/web-platform/tests/webtransport/handlers/query.py b/testing/web-platform/tests/webtransport/handlers/query.py new file mode 100644 index 0000000000..75d458255a --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/query.py @@ -0,0 +1,19 @@ +from typing import Optional +from urllib.parse import urlsplit, parse_qsl +import json + + +def session_established(session): + path: Optional[bytes] = None + for key, value in session.request_headers: + if key == b':path': + path = value + assert path is not None + qs = dict(parse_qsl(urlsplit(path).query)) + token = qs[b'token'] + if token is None: + raise Exception('token is missing, path = {}'.format(path)) + + stream_id = session.create_unidirectional_stream() + data = json.dumps(session.stash.take(key=token) or {}).encode('utf-8') + session.send_stream_data(stream_id, data, end_stream=True) diff --git a/testing/web-platform/tests/webtransport/handlers/server-close.py b/testing/web-platform/tests/webtransport/handlers/server-close.py new file mode 100644 index 0000000000..e9d08f483c --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/server-close.py @@ -0,0 +1,16 @@ +from typing import Optional +from urllib.parse import urlsplit, parse_qsl + + +def session_established(session): + path: Optional[bytes] = None + for key, value in session.request_headers: + if key == b':path': + path = value + assert path is not None + qs = dict(parse_qsl(urlsplit(path).query)) + code = qs[b'code'] if b'code' in qs else None + reason = qs[b'reason'] if b'reason' in qs else b'' + close_info = None if code is None else (int(code), reason) + + session.close(close_info) diff --git a/testing/web-platform/tests/webtransport/handlers/server-connection-close.py b/testing/web-platform/tests/webtransport/handlers/server-connection-close.py new file mode 100644 index 0000000000..9721c67170 --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/server-connection-close.py @@ -0,0 +1,9 @@ +def session_established(session): + session.create_bidirectional_stream() + + +def stream_data_received(session, + stream_id: int, + data: bytes, + stream_ended: bool): + session._http._quic.close()
\ No newline at end of file diff --git a/testing/web-platform/tests/webtransport/idlharness.https.any.js b/testing/web-platform/tests/webtransport/idlharness.https.any.js new file mode 100644 index 0000000000..248bc41abe --- /dev/null +++ b/testing/web-platform/tests/webtransport/idlharness.https.any.js @@ -0,0 +1,24 @@ +// META: global=window,worker +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +'use strict'; + +idl_test( + ['webtransport'], + ['webidl', 'streams'], + idl_array => { + idl_array.add_objects({ + WebTransport: ['webTransport'], + // TODO: The stream APIs below require a working connection to create. + // BidirectionalStream + // SendStream + // ReceiveStream + }); + self.webTransport = new WebTransport("https://example.com/"); + // `ready` and `closed` promises will be rejected due to connection error. + // Catches them to avoid unhandled rejections. + self.webTransport.ready.catch(() => {}); + self.webTransport.closed.catch(() => {}); + } +); diff --git a/testing/web-platform/tests/webtransport/in-removed-iframe.https.html b/testing/web-platform/tests/webtransport/in-removed-iframe.https.html new file mode 100644 index 0000000000..76204ac6a1 --- /dev/null +++ b/testing/web-platform/tests/webtransport/in-removed-iframe.https.html @@ -0,0 +1,25 @@ +<!doctype html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +'use strict'; +setup({ single_test: true }); + +function iframeOnLoad() { + const target = document.querySelector('#target'); + const wt = target.contentWindow.wt; + target.remove(); + const streams = wt.incomingBidirectionalStreams; + assert_equals(typeof streams, 'object', 'streams should be an object'); + done(); +} +</script> + +<iframe id=target onload="iframeOnLoad()" srcdoc=" +<!doctype html> +<script src=/common/get-host-info.sub.js></script> +<script src=resources/webtransport-test-helpers.sub.js></script> +<script> +window.wt = new WebTransport(webtransport_url('echo.py')); +</script> +"></iframe> diff --git a/testing/web-platform/tests/webtransport/resources/webtransport-test-helpers.sub.js b/testing/web-platform/tests/webtransport/resources/webtransport-test-helpers.sub.js new file mode 100644 index 0000000000..9f9127b22f --- /dev/null +++ b/testing/web-platform/tests/webtransport/resources/webtransport-test-helpers.sub.js @@ -0,0 +1,112 @@ +// The file including this must also include /common/get-host-info.sub.js to +// pick up the necessary constants. + +const HOST = get_host_info().ORIGINAL_HOST; +const PORT = '{{ports[webtransport-h3][0]}}'; +const BASE = `https://${HOST}:${PORT}`; + +// Wait for the given number of milliseconds (ms). +function wait(ms) { return new Promise(res => step_timeout(res, ms)); } + +// Create URL for WebTransport session. +function webtransport_url(handler) { + return `${BASE}/webtransport/handlers/${handler}`; +} + +// Converts WebTransport stream error code to HTTP/3 error code. +// https://ietf-wg-webtrans.github.io/draft-ietf-webtrans-http3/draft-ietf-webtrans-http3.html#section-4.3 +function webtransport_code_to_http_code(n) { + const first = 0x52e4a40fa8db; + return first + n + Math.floor(n / 0x1e); +} + +// Read all chunks from |readable_stream| and return as an array of arrays +async function read_stream(readable_stream) { + const reader = readable_stream.getReader(); + + let chunks = []; + while (true) { + const {value: chunk, done} = await reader.read(); + if (done) { + break; + } + chunks.push(chunk); + } + reader.releaseLock(); + + return chunks; +} + +// Read all chunks from |readable_stream|, decode chunks to a utf-8 string, then +// return the string. +async function read_stream_as_string(readable_stream) { + const decoder = new TextDecoderStream(); + const decode_stream = readable_stream.pipeThrough(decoder); + const reader = decode_stream.getReader(); + + let chunks = ''; + while (true) { + const {value: chunk, done} = await reader.read(); + if (done) { + break; + } + chunks += chunk; + } + reader.releaseLock(); + + return chunks; +} + +// Decode all chunks in a given ReadableStream, and parse the data using JSON. +async function read_stream_as_json(readable_stream) { + const text = await read_stream_as_string(readable_stream); + return JSON.parse(text); +} + +// Check the standard request headers and delete them, leaving any "unique" +// headers to check in the test. +function check_and_remove_standard_headers(headers) { + assert_equals(headers[':scheme'], 'https'); + delete headers[':scheme']; + assert_equals(headers[':method'], 'CONNECT'); + delete headers[':method']; + assert_equals(headers[':authority'], `${HOST}:${PORT}`); + delete headers[':authority']; + assert_equals(headers[':path'], '/webtransport/handlers/echo-request-headers.py'); + delete headers[':path']; + assert_equals(headers[':protocol'], 'webtransport'); + delete headers[':protocol']; + assert_equals(headers['origin'], `${get_host_info().ORIGIN}`); + delete headers['origin']; +} + +async function query(token) { + const wt = new WebTransport(webtransport_url(`query.py?token=${token}`)); + try { + await wt.ready; + const streams = await wt.incomingUnidirectionalStreams; + const streams_reader = streams.getReader(); + const { value: readable } = await streams_reader.read(); + streams_reader.releaseLock(); + + return await read_stream_as_json(readable); + } finally { + wt.close(); + } +} + +async function readInto(reader, buffer) { + let offset = 0; + + while (offset < buffer.byteLength) { + const {value: view, done} = await reader.read( + new Uint8Array(buffer, offset, buffer.byteLength - offset)); + buffer = view.buffer; + if (done) { + break; + } + offset += view.byteLength; + } + + return buffer; +} diff --git a/testing/web-platform/tests/webtransport/server-certificate-hashes.https.any.js b/testing/web-platform/tests/webtransport/server-certificate-hashes.https.any.js new file mode 100644 index 0000000000..01efab1191 --- /dev/null +++ b/testing/web-platform/tests/webtransport/server-certificate-hashes.https.any.js @@ -0,0 +1,21 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/webtransport-test-helpers.sub.js + +promise_test(async t => { + const hashValue = new Uint8Array(32); + // The connection fails because the fingerprint does not match. + const wt = new WebTransport(webtransport_url('echo.py'), { + serverCertificateHashes: [ + { + algorithm: "sha-256", + value: hashValue, + } + ] + }); + + const e = await wt.ready.catch(e => e); + await promise_rejects_exactly(t, e, wt.ready, 'ready should be rejected'); + await promise_rejects_exactly(t, e, wt.closed, 'closed should be rejected'); + assert_true(e instanceof WebTransportError); +}, 'Connection fails due to certificate hash mismatch'); diff --git a/testing/web-platform/tests/webtransport/streams-close.https.any.js b/testing/web-platform/tests/webtransport/streams-close.https.any.js new file mode 100644 index 0000000000..4871ee8e7b --- /dev/null +++ b/testing/web-platform/tests/webtransport/streams-close.https.any.js @@ -0,0 +1,252 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=resources/webtransport-test-helpers.sub.js + +// Note: There is no aioquic event for STOP_SENDING yet, so the server does +// not support checking this yet. Hence, tests checking from the STOP_SENDING +// signal cannot be tested yet. + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const bidi_stream = await wt.createBidirectionalStream(); + + const writable = bidi_stream.writable; + writable.close(); + + await wait(10); + const data = await query(id); + + assert_own_property(data, 'stream-close-info'); + const info = data['stream-close-info']; + + assert_equals(info.source, 'FIN', 'source'); +}, 'Close outgoing stream / bidi-1'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const streams_reader = wt.incomingBidirectionalStreams.getReader(); + const {value: bidi} = await streams_reader.read(); + + const writable = bidi.writable; + writable.close(); + + await wait(10); + const data = await query(id); + + assert_own_property(data, 'stream-close-info'); + const info = data['stream-close-info']; + + assert_equals(info.source, 'FIN', 'source'); +}, 'Close outgoing stream / bidi-2'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const writable = await wt.createUnidirectionalStream(); + writable.close(); + + await wait(10); + const data = await query(id); + + assert_own_property(data, 'stream-close-info'); + const info = data['stream-close-info']; + + assert_equals(info.source, 'FIN', 'source'); +}, 'Close outgoing stream / uni'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const bidi_stream = await wt.createBidirectionalStream(); + + const writable = bidi_stream.writable; + + const WT_CODE = 139; + const HTTP_CODE = webtransport_code_to_http_code(WT_CODE); + await writable.abort( + new WebTransportError({streamErrorCode: WT_CODE})); + + await wait(10); + const data = await query(id); + + // Check that stream is aborted with RESET_STREAM with the code and reason + assert_own_property(data, 'stream-close-info'); + const info = data['stream-close-info']; + + assert_equals(info.source, 'reset', 'reset stream'); + assert_equals(info.code, HTTP_CODE, 'code'); +}, 'Abort client-created bidirectional stream'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const stream_reader = wt.incomingBidirectionalStreams.getReader(); + const { value: bidi_stream } = await stream_reader.read(); + stream_reader.releaseLock(); + + const writer = bidi_stream.writable.getWriter(); + + const WT_CODE = 52; + const HTTP_CODE = webtransport_code_to_http_code(WT_CODE); + await writer.abort( + new WebTransportError({streamErrorCode: WT_CODE})); + + await wait(10); + const data = await query(id); + + // Check that stream is aborted with RESET_STREAM with the code and reason + assert_own_property(data, 'stream-close-info'); + const info = data['stream-close-info']; + + assert_equals(info.source, 'reset', 'reset_stream'); + assert_equals(info.code, HTTP_CODE, 'code'); +}, 'Abort server-initiated bidirectional stream'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const writable = await wt.createUnidirectionalStream(); + + const WT_CODE = 95; + const HTTP_CODE = webtransport_code_to_http_code(WT_CODE); + await writable.abort( + new WebTransportError({streamErrorCode: WT_CODE})); + + await wait(10); + const data = await query(id); + + // Check that stream is aborted with RESET_STREAM with the code and reason + assert_own_property(data, 'stream-close-info'); + const info = data['stream-close-info']; + + assert_equals(info.source, 'reset', 'reset_stream'); + assert_equals(info.code, HTTP_CODE, 'code'); +}, 'Abort unidirectional stream with WebTransportError'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const writable = await wt.createUnidirectionalStream(); + const writer = writable.getWriter(); + + const WT_CODE = 134; + const HTTP_CODE = webtransport_code_to_http_code(WT_CODE); + + // We use a large chunk so that sending the FIN signal takes time. + const chunk = new Uint8Array(64 * 1024); + const e = new WebTransportError({streamErrorCode: WT_CODE}); + // Write a chunk, close the stream, and then abort the stream immediately to + // abort the closing operation. + // TODO: Check that the abort promise is correctly rejected/resolved based on + // the spec discussion at https://github.com/whatwg/streams/issues/1203. + await writer.write(chunk); + const close_promise = writer.close(); + const abort_promise = writer.abort(e); + + await promise_rejects_exactly(t, e, close_promise, 'close_promise'); + await promise_rejects_exactly(t, e, writer.closed, '.closed'); + await promise_rejects_exactly(t, e, abort_promise, 'abort_promise'); + writer.releaseLock(); + + await wait(10); + const data = await query(id); + + // Check that stream is aborted with RESET_STREAM with the code and reason + assert_own_property(data, 'stream-close-info'); + const info = data['stream-close-info']; + + assert_equals(info.source, 'reset', 'reset_stream'); + assert_equals(info.code, HTTP_CODE, 'code'); +}, 'Close and abort unidirectional stream'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const writable = await wt.createUnidirectionalStream(); + await writable.abort(); + + await wait(10); + const data = await query(id); + + // Check that stream is aborted with RESET_STREAM with the code and reason + assert_own_property(data, 'stream-close-info'); + const info = data['stream-close-info']; + + assert_equals(info.source, 'reset', 'reset_stream'); + assert_equals(info.code, webtransport_code_to_http_code(0), 'code'); +}, 'Abort unidirectional stream with default error code'); + +promise_test(async t => { + const WT_CODE = 240; + const HTTP_CODE = webtransport_code_to_http_code(WT_CODE); + const wt = new WebTransport( + webtransport_url(`abort-stream-from-server.py?code=${HTTP_CODE}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const writable = await wt.createUnidirectionalStream(); + const writer = writable.getWriter(); + + // Write something, to make the stream visible to the server side. + await writer.write(new Uint8Array([64])); + + // Sadly we cannot use promise_rejects_dom as the error constructor is + // WebTransportError rather than DOMException. Ditto below. + // We get a possible error, and then make sure wt.closed is rejected with it. + const e = await writer.closed.catch(e => e); + await promise_rejects_exactly( + t, e, writer.closed, 'closed promise should be rejected'); + assert_true(e instanceof WebTransportError); + assert_equals(e.source, 'stream', 'source'); + assert_equals(e.streamErrorCode, WT_CODE, 'streamErrorCode'); +}, 'STOP_SENDING coming from server'); + +promise_test(async t => { + const WT_CODE = 127; + const HTTP_CODE = webtransport_code_to_http_code(WT_CODE); + const wt = new WebTransport( + webtransport_url(`abort-stream-from-server.py?code=${HTTP_CODE}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const bidi = await wt.createBidirectionalStream(); + const writer = bidi.writable.getWriter(); + + // Write something, to make the stream visible to the server side. + await writer.write(new Uint8Array([64])); + + const reader = bidi.readable.getReader(); + const e = await reader.closed.catch(e => e); + await promise_rejects_exactly( + t, e, reader.closed, 'closed promise should be rejected'); + assert_true(e instanceof WebTransportError); + assert_equals(e.source, 'stream', 'source'); + assert_equals(e.streamErrorCode, WT_CODE, 'streamErrorCode'); +}, 'RESET_STREAM coming from server'); diff --git a/testing/web-platform/tests/webtransport/streams-echo.https.any.js b/testing/web-platform/tests/webtransport/streams-echo.https.any.js new file mode 100644 index 0000000000..703318d0e7 --- /dev/null +++ b/testing/web-platform/tests/webtransport/streams-echo.https.any.js @@ -0,0 +1,282 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/webtransport-test-helpers.sub.js + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + // Create a bidirectional stream. + const bidi_stream = await wt.createBidirectionalStream(); + + // Write a message to the writable end, and close it. + const writer = bidi_stream.writable.getWriter(); + const encoder = new TextEncoder(); + await writer.write(encoder.encode('Hello World')); + await writer.close(); + + // Read the data on the readable end. + const reply = await read_stream_as_string(bidi_stream.readable); + + // Check that the message from the readable end matches the writable end. + assert_equals(reply, 'Hello World'); +}, 'WebTransport client should be able to create and handle a bidirectional stream'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + + // Create a bidirectional stream. + const bidi_stream = await wt.createBidirectionalStream(); + + // Write a message to the writable end, and close it. + const writer = bidi_stream.writable.getWriter(); + const encoder = new TextEncoder(); + await writer.write(encoder.encode('Hello World')); + await writer.close(); + + // Read the data on the readable end. + const reply = await read_stream_as_string(bidi_stream.readable); + + // Check that the message from the readable end matches the writable end. + assert_equals(reply, 'Hello World'); +}, 'WebTransport client should be able to create and handle a bidirectional stream without waiting for ready'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + // The echo handler creates a bidirectional stream when a WebTransport session + // is established. Accept the bidirectional stream. + const stream_reader = wt.incomingBidirectionalStreams.getReader(); + const { value: bidi_stream } = await stream_reader.read(); + stream_reader.releaseLock(); + + // Write a message to the writable end, and close it. + const encoder = new TextEncoderStream(); + encoder.readable.pipeTo(bidi_stream.writable); + const writer = encoder.writable.getWriter(); + await writer.write('Hello World'); + await writer.close(); + + // Read the data on the readable end. + const reply = await read_stream_as_string(bidi_stream.readable); + + // Check that the message from the readable end matches the writable end. + assert_equals(reply, 'Hello World'); +}, 'WebTransport server should be able to accept and handle a bidirectional stream'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + // Create a unidirectional stream. + const writable = await wt.createUnidirectionalStream(); + + // Write a message to the writable end, and close it. + const encoder = new TextEncoderStream(); + encoder.readable.pipeTo(writable); + const writer = encoder.writable.getWriter(); + await writer.write('Hello World'); + await writer.close(); + + // The echo handler creates a new unidirectional stream to echo back data from + // the server to client. Accept the unidirectional stream. + const readable = wt.incomingUnidirectionalStreams; + const stream_reader = readable.getReader(); + const { value: recv_stream } = await stream_reader.read(); + stream_reader.releaseLock(); + + // Read the data on the readable end. + const reply = await read_stream_as_string(recv_stream); + + // Make sure the message on the writable and readable ends of the streams + // match. + assert_equals(reply, 'Hello World'); +}, 'WebTransport client should be able to create, accept, and handle a unidirectional stream'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + + // Create a unidirectional stream. + const writable = await wt.createUnidirectionalStream(); + + // Write a message to the writable end, and close it. + const encoder = new TextEncoderStream(); + encoder.readable.pipeTo(writable); + const writer = encoder.writable.getWriter(); + await writer.write('Hello World'); + await writer.close(); + + // The echo handler creates a new unidirectional stream to echo back data from + // the server to client. Accept the unidirectional stream. + const readable = wt.incomingUnidirectionalStreams; + const stream_reader = readable.getReader(); + const { value: recv_stream } = await stream_reader.read(); + stream_reader.releaseLock(); + + // Read the data on the readable end. + const reply = await read_stream_as_string(recv_stream); + + // Make sure the message on the writable and readable ends of the streams + // match. + assert_equals(reply, 'Hello World'); +}, 'WebTransport client should be able to create, accept, and handle a unidirectional stream without waiting for ready'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + // The echo handler creates a bidirectional stream when a WebTransport session + // is established. Accept the bidirectional stream. + const stream_reader = wt.incomingBidirectionalStreams.getReader(); + const {value: bidi_stream} = await stream_reader.read(); + stream_reader.releaseLock(); + + // Write data to the writable end, and close it. + const buffer_size = 256; + const data = new Uint8Array(buffer_size); + for (let i = 0; i < data.byteLength; ++i) { + data[i] = i; + } + const writer = bidi_stream.writable.getWriter(); + writer.write(data); + await writer.close(); + + // Read the data on the readable end and check if it matches the writable end. + const reader = bidi_stream.readable.getReader({mode: 'byob'}); + assert_true(reader instanceof ReadableStreamBYOBReader); + const half_buffer_size = buffer_size / 2; + for (let i = 0; i < 2; i++) { + let buffer = new ArrayBuffer(half_buffer_size); + buffer = await readInto(reader, buffer); + assert_array_equals( + new Uint8Array(buffer), + data.subarray(half_buffer_size * i, half_buffer_size * (i + 1))) + } + reader.releaseLock(); +}, 'Can read data from a bidirectional stream with BYOB reader'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + // Create a unidirectional stream. + const writable = await wt.createUnidirectionalStream(); + + // Write data to the writable end, and close it. + const buffer_size = 256; + const data = new Uint8Array(buffer_size); + for (let i = 0; i < data.byteLength; ++i) { + data[i] = i; + } + const writer = writable.getWriter(); + writer.write(data); + await writer.close(); + + // The echo handler creates a new unidirectional stream to echo back data from + // the server to client. Accept the unidirectional stream. + const readable = wt.incomingUnidirectionalStreams; + const stream_reader = readable.getReader(); + const {value: recv_stream} = await stream_reader.read(); + stream_reader.releaseLock(); + + // Read the data on the readable end and check if it matches the writable end. + const reader = recv_stream.getReader({mode: 'byob'}); + assert_true(reader instanceof ReadableStreamBYOBReader); + const half_buffer_size = buffer_size / 2; + let buffer = new ArrayBuffer(half_buffer_size); + for (let i = 0; i < 2; i++) { + buffer = await readInto(reader, buffer); + assert_array_equals( + new Uint8Array(buffer), + data.subarray(half_buffer_size * i, half_buffer_size * (i + 1))) + } + reader.releaseLock(); +}, 'Can read data from a unidirectional stream with BYOB reader'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + // Create a bidirectional stream. + const bidi_stream = await wt.createBidirectionalStream(); + + // Write a message to the writable end, and close it. + const writer = bidi_stream.writable.getWriter(); + const bytes = new Uint8Array(16384); + const [reply] = await Promise.all([ + read_stream(bidi_stream.readable), + writer.write(bytes), + writer.write(bytes), + writer.write(bytes), + writer.close() + ]); + let len = 0; + for (chunk of reply) { + len += chunk.length; + } + // Check that the message from the readable end matches the writable end. + assert_equals(len, 3*bytes.length); +}, 'Transfer large chunks of data on a bidirectional stream'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + // Create a unidirectional stream. + const uni_stream = await wt.createUnidirectionalStream(); + + // Write a message to the writable end, and close it. + const writer = uni_stream.getWriter(); + const bytes = new Uint8Array(16384); + await Promise.all([ + writer.write(bytes), + writer.write(bytes), + writer.write(bytes), + writer.close() + ]); + // XXX Update once chrome fixes https://crbug.com/929585 + // The echo handler creates a new unidirectional stream to echo back data from + // the server to client. Accept the unidirectional stream. + const readable = wt.incomingUnidirectionalStreams; + const stream_reader = readable.getReader(); + const { value: recv_stream } = await stream_reader.read(); + stream_reader.releaseLock(); + + // Read the data on the readable end. + const reply = await read_stream(recv_stream); + let len = 0; + for (chunk of reply) { + len += chunk.length; + } + // Check that the message from the readable end matches the writable end. + assert_equals(len, 3*bytes.length); +}, 'Transfer large chunks of data on a unidirectional stream'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + // Create a bidirectional stream. + const bidi_stream = await wt.createBidirectionalStream(); + + // Close the writable end with no data at all. + const writer = bidi_stream.writable.getWriter(); + writer.close(); + + // Read the data on the readable end. + const chunks = await read_stream(bidi_stream.readable); + assert_equals(chunks.length, 0); + + await bidi_stream.readable.closed; +}, 'Closing the stream with no data still resolves the read request'); |