Skip to content

Commit ad251bb

Browse files
authored
feat: add in WPTs (nodejs#1698)
1 parent 4b30d47 commit ad251bb

14 files changed

+532
-0
lines changed

test/wpt/server/server.mjs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,84 @@ const server = createServer(async (req, res) => {
124124
res.end()
125125
break
126126
}
127+
case '/fetch/api/resources/status.py': {
128+
const code = parseInt(fullUrl.searchParams.get('code') ?? 200)
129+
const text = fullUrl.searchParams.get('text') ?? 'OMG'
130+
const content = fullUrl.searchParams.get('content') ?? ''
131+
const type = fullUrl.searchParams.get('type') ?? ''
132+
res.statusCode = code
133+
res.statusMessage = text
134+
res.setHeader('Content-Type', type)
135+
res.setHeader('X-Request-Method', req.method)
136+
res.end(content)
137+
break
138+
}
139+
case '/fetch/api/resources/inspect-headers.py': {
140+
const query = fullUrl.searchParams
141+
const checkedHeaders = query.get('headers')
142+
?.split('|')
143+
.map(h => h.toLowerCase()) ?? []
144+
145+
if (query.has('headers')) {
146+
for (const header of checkedHeaders) {
147+
if (Object.hasOwn(req.headers, header)) {
148+
res.setHeader(`x-request-${header}`, req.headers[header] ?? '')
149+
}
150+
}
151+
}
152+
153+
if (query.has('cors')) {
154+
if (Object.hasOwn(req.headers, 'origin')) {
155+
res.setHeader('Access-Control-Allow-Origin', req.headers.origin ?? '')
156+
} else {
157+
res.setHeader('Access-Control-Allow-Origin', '*')
158+
}
159+
160+
res.setHeader('Access-Control-Allow-Credentials', 'true')
161+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, HEAD')
162+
const exposedHeaders = checkedHeaders.map(h => `x-request-${h}`).join(', ')
163+
res.setHeader('Access-Control-Expose-Headers', exposedHeaders)
164+
if (query.has('allow_headers')) {
165+
res.setHeader('Access-Control-Allow-Headers', query.get('allowed_headers'))
166+
} else {
167+
res.setHeader('Access-Control-Allow-Headers', Object.keys(req.headers).join(', '))
168+
}
169+
}
170+
171+
res.setHeader('content-type', 'text/plain')
172+
res.end('')
173+
break
174+
}
175+
case '/xhr/resources/parse-headers.py': {
176+
if (fullUrl.searchParams.has('my-custom-header')) {
177+
const val = fullUrl.searchParams.get('my-custom-header').toLowerCase()
178+
// res.setHeader does validation which may prevent some tests from running.
179+
res.socket.write(
180+
`HTTP/1.1 200 OK\r\nmy-custom-header: ${val}\r\n\r\n`
181+
)
182+
}
183+
res.end('')
184+
break
185+
}
186+
case '/fetch/api/resources/bad-chunk-encoding.py': {
187+
const query = fullUrl.searchParams
188+
189+
const delay = parseFloat(query.get('ms') ?? 1000)
190+
const count = parseInt(query.get('count') ?? 50)
191+
await sleep(delay)
192+
res.socket.write(
193+
'HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n'
194+
)
195+
await sleep(delay)
196+
197+
for (let i = 0; i < count; i++) {
198+
res.socket.write('a\r\nTEST_CHUNK\r\n')
199+
await sleep(delay)
200+
}
201+
202+
res.end('garbage')
203+
break
204+
}
127205
default: {
128206
res.statusCode = 200
129207
res.end('body')

test/wpt/status/fetch.status.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,10 @@
3535
"Check response clone use structureClone for teed ReadableStreams (ArrayBufferchunk)",
3636
"Check response clone use structureClone for teed ReadableStreams (DataViewchunk)"
3737
]
38+
},
39+
"request-upload.any.js": {
40+
"fail": [
41+
"Fetch with POST with text body on 421 response should be retried once on new connection."
42+
]
3843
}
3944
}

test/wpt/tests/LICENSE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# The 3-Clause BSD License
2+
3+
Copyright © web-platform-tests contributors
4+
5+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
6+
7+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10+
11+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// META: global=window,worker
2+
// META: script=../resources/utils.js
3+
4+
promise_test(function() {
5+
return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept").then(function(response) {
6+
assert_equals(response.status, 200, "HTTP status is 200");
7+
assert_equals(response.type , "basic", "Response's type is basic");
8+
assert_equals(response.headers.get("x-request-accept"), "*/*", "Request has accept header with value '*/*'");
9+
});
10+
}, "Request through fetch should have 'accept' header with value '*/*'");
11+
12+
promise_test(function() {
13+
return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept", {"headers": [["Accept", "custom/*"]]}).then(function(response) {
14+
assert_equals(response.status, 200, "HTTP status is 200");
15+
assert_equals(response.type , "basic", "Response's type is basic");
16+
assert_equals(response.headers.get("x-request-accept"), "custom/*", "Request has accept header with value 'custom/*'");
17+
});
18+
}, "Request through fetch should have 'accept' header with value 'custom/*'");
19+
20+
promise_test(function() {
21+
return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept-Language").then(function(response) {
22+
assert_equals(response.status, 200, "HTTP status is 200");
23+
assert_equals(response.type , "basic", "Response's type is basic");
24+
assert_true(response.headers.has("x-request-accept-language"));
25+
});
26+
}, "Request through fetch should have a 'accept-language' header");
27+
28+
promise_test(function() {
29+
return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept-Language", {"headers": [["Accept-Language", "bzh"]]}).then(function(response) {
30+
assert_equals(response.status, 200, "HTTP status is 200");
31+
assert_equals(response.type , "basic", "Response's type is basic");
32+
assert_equals(response.headers.get("x-request-accept-language"), "bzh", "Request has accept header with value 'bzh'");
33+
});
34+
}, "Request through fetch should have 'accept-language' header with value 'bzh'");
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// META: title=Fetch: network timeout after receiving the HTTP response headers
2+
// META: global=window,worker
3+
// META: timeout=long
4+
// META: script=../resources/utils.js
5+
6+
function checkReader(test, reader, promiseToTest)
7+
{
8+
return reader.read().then((value) => {
9+
validateBufferFromString(value.value, "TEST_CHUNK", "Should receive first chunk");
10+
return promise_rejects_js(test, TypeError, promiseToTest(reader));
11+
});
12+
}
13+
14+
promise_test((test) => {
15+
return fetch("../resources/bad-chunk-encoding.py?count=1").then((response) => {
16+
return checkReader(test, response.body.getReader(), reader => reader.read());
17+
});
18+
}, "Response reader read() promise should reject after a network error happening after resolving fetch promise");
19+
20+
promise_test((test) => {
21+
return fetch("../resources/bad-chunk-encoding.py?count=1").then((response) => {
22+
return checkReader(test, response.body.getReader(), reader => reader.closed);
23+
});
24+
}, "Response reader closed promise should reject after a network error happening after resolving fetch promise");
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// META: global=window,worker
2+
3+
promise_test(t => {
4+
return promise_rejects_js(t, TypeError, fetch("../../../xhr/resources/parse-headers.py?my-custom-header="+encodeURIComponent("x\0x")));
5+
}, "Ensure fetch() rejects null bytes in headers");
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// META: global=window,worker
2+
3+
test(() => {
4+
assert_false("getAll" in new Headers());
5+
assert_false("getAll" in Headers.prototype);
6+
}, "Headers object no longer has a getAll() method");
7+
8+
test(() => {
9+
assert_false("type" in new Request("about:blank"));
10+
assert_false("type" in Request.prototype);
11+
}, "'type' getter should not exist on Request objects");
12+
13+
// See https://github.com/whatwg/fetch/pull/979 for the removal
14+
test(() => {
15+
assert_false("trailer" in new Response());
16+
assert_false("trailer" in Response.prototype);
17+
}, "Response object no longer has a trailer getter");
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// META: global=window,worker
2+
3+
promise_test(function(test) {
4+
var requestInit = {"method": "HEAD", "body": "test"};
5+
return promise_rejects_js(test, TypeError, fetch(".", requestInit));
6+
}, "Fetch with HEAD with body");
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// META: global=window,worker
2+
3+
// This tests characters that are not
4+
// https://infra.spec.whatwg.org/#ascii-code-point
5+
// but are still
6+
// https://infra.spec.whatwg.org/#byte-value
7+
// in request header values.
8+
// Such request header values are valid and thus sent to servers.
9+
// Characters outside the #byte-value range are tested e.g. in
10+
// fetch/api/headers/headers-errors.html.
11+
12+
promise_test(() => {
13+
return fetch(
14+
"../resources/inspect-headers.py?headers=accept|x-test",
15+
{headers: {
16+
"Accept": "before-æøå-after",
17+
"X-Test": "before-ß-after"
18+
}})
19+
.then(res => {
20+
assert_equals(
21+
res.headers.get("x-request-accept"),
22+
"before-æøå-after",
23+
"Accept Header");
24+
assert_equals(
25+
res.headers.get("x-request-x-test"),
26+
"before-ß-after",
27+
"X-Test Header");
28+
});
29+
}, "Non-ascii bytes in request headers");
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// META: global=window,worker
2+
// META: script=../resources/utils.js
3+
// META: script=/common/utils.js
4+
// META: script=/common/get-host-info.sub.js
5+
6+
function testUpload(desc, url, method, createBody, expectedBody) {
7+
const requestInit = {method};
8+
promise_test(function(test){
9+
const body = createBody();
10+
if (body) {
11+
requestInit["body"] = body;
12+
requestInit.duplex = "half";
13+
}
14+
return fetch(url, requestInit).then(function(resp) {
15+
return resp.text().then((text)=> {
16+
assert_equals(text, expectedBody);
17+
});
18+
});
19+
}, desc);
20+
}
21+
22+
function testUploadFailure(desc, url, method, createBody) {
23+
const requestInit = {method};
24+
promise_test(t => {
25+
const body = createBody();
26+
if (body) {
27+
requestInit["body"] = body;
28+
}
29+
return promise_rejects_js(t, TypeError, fetch(url, requestInit));
30+
}, desc);
31+
}
32+
33+
const url = RESOURCES_DIR + "echo-content.py"
34+
35+
testUpload("Fetch with PUT with body", url,
36+
"PUT",
37+
() => "Request's body",
38+
"Request's body");
39+
testUpload("Fetch with POST with text body", url,
40+
"POST",
41+
() => "Request's body",
42+
"Request's body");
43+
testUpload("Fetch with POST with URLSearchParams body", url,
44+
"POST",
45+
() => new URLSearchParams("name=value"),
46+
"name=value");
47+
testUpload("Fetch with POST with Blob body", url,
48+
"POST",
49+
() => new Blob(["Test"]),
50+
"Test");
51+
testUpload("Fetch with POST with ArrayBuffer body", url,
52+
"POST",
53+
() => new ArrayBuffer(4),
54+
"\0\0\0\0");
55+
testUpload("Fetch with POST with Uint8Array body", url,
56+
"POST",
57+
() => new Uint8Array(4),
58+
"\0\0\0\0");
59+
testUpload("Fetch with POST with Int8Array body", url,
60+
"POST",
61+
() => new Int8Array(4),
62+
"\0\0\0\0");
63+
testUpload("Fetch with POST with Float32Array body", url,
64+
"POST",
65+
() => new Float32Array(1),
66+
"\0\0\0\0");
67+
testUpload("Fetch with POST with Float64Array body", url,
68+
"POST",
69+
() => new Float64Array(1),
70+
"\0\0\0\0\0\0\0\0");
71+
testUpload("Fetch with POST with DataView body", url,
72+
"POST",
73+
() => new DataView(new ArrayBuffer(8), 0, 4),
74+
"\0\0\0\0");
75+
testUpload("Fetch with POST with Blob body with mime type", url,
76+
"POST",
77+
() => new Blob(["Test"], { type: "text/maybe" }),
78+
"Test");
79+
80+
testUploadFailure("Fetch with POST with ReadableStream containing String", url,
81+
"POST",
82+
() => {
83+
return new ReadableStream({start: controller => {
84+
controller.enqueue("Test");
85+
controller.close();
86+
}})
87+
});
88+
testUploadFailure("Fetch with POST with ReadableStream containing null", url,
89+
"POST",
90+
() => {
91+
return new ReadableStream({start: controller => {
92+
controller.enqueue(null);
93+
controller.close();
94+
}})
95+
});
96+
testUploadFailure("Fetch with POST with ReadableStream containing number", url,
97+
"POST",
98+
() => {
99+
return new ReadableStream({start: controller => {
100+
controller.enqueue(99);
101+
controller.close();
102+
}})
103+
});
104+
testUploadFailure("Fetch with POST with ReadableStream containing ArrayBuffer", url,
105+
"POST",
106+
() => {
107+
return new ReadableStream({start: controller => {
108+
controller.enqueue(new ArrayBuffer());
109+
controller.close();
110+
}})
111+
});
112+
testUploadFailure("Fetch with POST with ReadableStream containing Blob", url,
113+
"POST",
114+
() => {
115+
return new ReadableStream({start: controller => {
116+
controller.enqueue(new Blob());
117+
controller.close();
118+
}})
119+
});
120+
121+
promise_test(async (test) => {
122+
const resp = await fetch(
123+
"/fetch/connection-pool/resources/network-partition-key.py?"
124+
+ `status=421&uuid=${token()}&partition_id=${get_host_info().ORIGIN}`
125+
+ `&dispatch=check_partition&addcounter=true`,
126+
{method: "POST", body: "foobar"});
127+
assert_equals(resp.status, 421);
128+
const text = await resp.text();
129+
assert_equals(text, "ok. Request was sent 2 times. 2 connections were created.");
130+
}, "Fetch with POST with text body on 421 response should be retried once on new connection.");
131+
132+
promise_test(async (test) => {
133+
const body = new ReadableStream({start: c => c.close()});
134+
await promise_rejects_js(test, TypeError, fetch('/', {method: 'POST', body}));
135+
}, "Streaming upload shouldn't work on Http/1.1.");

0 commit comments

Comments
 (0)