# HG changeset patch # User Dmitry Volyntsev # Date 1670950242 28800 # Node ID 1d88487eafbfae3758cee11ecd6aa2d297657816 # Parent 0a489a9abc7ab6f8042823bcf6b14e352f12ad21 Tests: added js tests for Fetch API objects. In addition, due to the fact that Headers.prototype.keys() now returns sorted header names one js_fetch.t test became broken. To fix it without introducing version check the test was changed so headers received from upstream are already sorted. diff --git a/js_fetch.t b/js_fetch.t --- a/js_fetch.t +++ b/js_fetch.t @@ -421,7 +421,7 @@ like(http_get('/header?loc=duplicate_hea 'fetch header'); like(http_get('/header?loc=duplicate_header&h=BARR'), qr/200 OK.*null$/s, 'fetch no header'); -like(http_get('/header?loc=duplicate_header&h=foo'), qr/200 OK.*a,b$/s, +like(http_get('/header?loc=duplicate_header&h=foo'), qr/200 OK.*a, ?b$/s, 'fetch header duplicate'); like(http_get('/header?loc=duplicate_header&h=BAR&method=getAll'), qr/200 OK.*\['c']$/s, 'fetch getAll header'); @@ -452,7 +452,7 @@ todo_skip 'leaves coredump', 1 unless $E or has_version('0.7.4'); like(http_get('/header_iter?loc=duplicate_header_large'), - qr/\['A:a','B:a','C:a','D:a','E:a','F:a','G:a','H:a','Foo:a,b']$/s, + qr/\['A:a','B:a','C:a','D:a','E:a','F:a','G:a','H:a','Moo:a, ?b']$/s, 'fetch header duplicate large'); } @@ -579,8 +579,8 @@ sub http_daemon { "F: a" . CRLF . "G: a" . CRLF . "H: a" . CRLF . - "Foo: a" . CRLF . - "Foo: b" . CRLF . + "Moo: a" . CRLF . + "Moo: b" . CRLF . "Connection: close" . CRLF . CRLF; diff --git a/js_fetch_objects.t b/js_fetch_objects.t new file mode 100644 --- /dev/null +++ b/js_fetch_objects.t @@ -0,0 +1,539 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, fetch objects. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http rewrite/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /njs { + js_content test.njs; + } + + location /headers { + js_content test.headers; + } + + location /request { + js_content test.request; + } + + location /response { + js_content test.response; + } + + location /fetch { + js_content test.fetch; + } + + location /method { + return 200 $request_method; + } + + location /header { + return 200 $http_a; + } + + location /body { + js_content test.body; + } + } +} + +EOF + +my $p0 = port(8080); + +$t->write_file('test.js', < { + var h = new Headers(); + return h.get('a'); + }, null], + ['normal', () => { + var h = new Headers({a: 'X', b: 'Z'}); + return `\${h.get('a')} \${h.get('B')}`; + }, 'X Z'], + ['trim value', () => { + var h = new Headers({a: ' X '}); + return h.get('a'); + }, 'X'], + ['invalid header name', () => { + const valid = "!#\$\%&'*+-.^_`|~0123456789"; + + for (var i = 0; i < 128; i++) { + var c = String.fromCodePoint(i); + + if (valid.indexOf(c) != -1 || /[a-zA-Z]+/.test(c)) { + continue; + } + + try { + new Headers([[c, 'a']]); + throw new Error( + `header with "\${c}" (\${i}) should throw`); + + } catch (e) { + if (e.message != 'invalid header name') { + throw e; + } + } + } + + return 'OK'; + + }, 'OK'], + ['invalid header value', () => { + var h = new Headers({A: 'aa\x00a'}); + }, 'invalid header value'], + ['forbidden header', () => { + const forbidden = ['Host', 'Connection', 'Content-length']; + forbidden.forEach(fh => { + var headers = {}; + headers[fh] = 'xxx'; + headers.foo = 'bar'; + + var h = new Headers(headers); + if (h.get(fh) != 'xxx') { + throw new Error(`forbidden header \${fh}`); + } + + if (h.get('foo') != 'bar') { + throw new Error( + `non forbidden header foo: \${h.get('foo')}`); + } + }) + + return 'OK'; + + }, 'OK'], + ['combine', () => { + var h = new Headers({a: 'X', A: 'Z'}); + return h.get('a'); + }, 'X, Z'], + ['combine2', () => { + var h = new Headers([['A', 'x'], ['a', 'z']]); + return h.get('a'); + }, 'x, z'], + ['combine3', () => { + var h = new Headers(); + h.append('a', 'A'); + h.append('a', 'B'); + h.append('a', 'C'); + h.append('a', 'D'); + h.append('a', 'E'); + h.append('a', 'F'); + return h.get('a'); + }, 'A, B, C, D, E, F'], + ['getAll', () => { + var h = new Headers({a: 'X', A: 'Z'}); + return njs.dump(h.getAll('a')); + }, "['X','Z']"], + ['inherit', () => { + var h = new Headers({a: 'X', b: 'Y'}); + var h2 = new Headers(h); + h2.append('c', 'Z'); + return h2.has('a') && h2.has('B') && h2.has('c'); + }, true], + ['delete', () => { + var h = new Headers({a: 'X', b: 'Z'}); + h.delete('b'); + return h.get('a') && !h.get('b'); + }, true], + ['forEach', () => { + var r = []; + var h = new Headers({a: '0', b: '1', c: '2'}); + h.delete('b'); + h.append('z', '3'); + h.append('a', '4'); + h.append('q', '5'); + h.forEach((v, k) => { r.push(`\${v}:\${k}`)}) + return r.join('|'); + }, 'a:0, 4|c:2|q:5|z:3'], + ['set', () => { + var h = new Headers([['A', 'x'], ['a', 'y'], ['a', 'z']]); + h.set('a', '#'); + return h.get('a'); + }, '#'], + ]; + + run(r, tests); + } + + async function request(r) { + const tests = [ + ['empty', () => { + try { + new Request(); + throw new Error(`Request() should throw`); + + } catch (e) { + if (e.message != '1st argument is required') { + throw e; + } + } + + return 'OK'; + + }, 'OK'], + ['normal', () => { + var r = new Request("http://nginx.org", + {headers: {a: 'X', b: 'Y'}}); + return `\${r.url}: \${r.method} \${r.headers.a}`; + }, 'http://nginx.org: GET X'], + ['url trim', () => { + var r = new Request("\\x00\\x01\\x02\\x03\\x05\\x06\\x07\\x08" + + "\\x09\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f" + + "\\x10\\x11\\x12\\x13\\x14\\x15\\x16" + + "\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d" + + "\\x1e\\x1f\\x20http://nginx.org\\x00" + + "\\x01\\x02\\x03\\x05\\x06\\x07\\x08" + + "\\x09\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f" + + "\\x10\\x11\\x12\\x13\\x14\\x15\\x16" + + "\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d" + + "\\x1e\\x1f\\x20"); + return r.url; + }, 'http://nginx.org'], + ['read only', () => { + var r = new Request("http://nginx.org"); + + const props = ['bodyUsed', 'cache', 'credentials', 'headers', + 'method', 'mode', 'url']; + try { + props.forEach(prop => { + r[prop] = 1; + throw new Error( + `setting read-only \${prop} should throw`); + }) + + } catch (e) { + if (!e.message.startsWith('Cannot assign to read-only p')) { + throw e; + } + } + + return 'OK'; + + }, 'OK'], + ['cache', () => { + const props = ['default', 'no-cache', 'no-store', 'reload', + 'force-cache', 'only-if-cached', '#']; + try { + props.forEach(cv => { + var r = new Request("http://nginx.org", {cache: cv}); + if (r.cache != cv) { + throw new Error(`r.cache != \${cv}`); + } + }) + + } catch (e) { + if (!e.message.startsWith('unknown cache type: #')) { + throw e; + } + } + + return 'OK'; + + }, 'OK'], + ['credentials', () => { + const props = ['omit', 'include', 'same-origin', '#']; + try { + props.forEach(cr => { + var r = new Request("http://nginx.org", + {credentials: cr}); + if (r.credentials != cr) { + throw new Error(`r.credentials != \${cr}`); + } + }) + + } catch (e) { + if (!e.message.startsWith('unknown credentials type: #')) { + throw e; + } + } + + return 'OK'; + + }, 'OK'], + ['forbidden request header', () => { + const forbidden = ['Host', 'Connection', 'Content-length']; + forbidden.forEach(fh => { + var r = new Request("http://nginx.org", + {headers: {[fh]: 'xxx', foo: 'bar'}}); + if (r.headers.get(fh) != null) { + throw new Error(`forbidden header \${fh}`); + } + + if (r.headers.get('foo') != 'bar') { + throw new Error( + `non forbidden header foo: \${r.headers.get('foo')}`); + } + }) + + return 'OK'; + + }, 'OK'], + ['method', () => { + const methods = ['get', 'hEad', 'Post', 'OPTIONS', 'PUT', + 'DELETE', 'CONNECT']; + try { + methods.forEach(m => { + var r = new Request("http://nginx.org", {method: m}); + if (r.method != m.toUpperCase()) { + throw new Error(`r.method != \${m}`); + } + }) + + } catch (e) { + if (!e.message.startsWith('forbidden method: CONNECT')) { + throw e; + } + } + + return 'OK'; + + }, 'OK'], + ['mode', () => { + const props = ['same-origin', 'cors', 'no-cors', 'navigate', + 'websocket', '#']; + try { + props.forEach(m => { + var r = new Request("http://nginx.org", {mode: m}); + if (r.mode != m) { + throw new Error(`r.mode != \${m}`); + } + }) + + } catch (e) { + if (!e.message.startsWith('unknown mode type: #')) { + throw e; + } + } + + return 'OK'; + + }, 'OK'], + ['inherit', () => { + var r = new Request("http://nginx.org", + {headers: {a: 'X', b: 'Y'}}); + var r2 = new Request(r); + r2.headers.append('a', 'Z') + return `\${r2.url}: \${r2.headers.get('a')}`; + }, 'http://nginx.org: X, Z'], + ['inherit2', () => { + var r = new Request("http://nginx.org", + {headers: {a: 'X', b: 'Y'}}); + var r2 = new Request(r); + r2.headers.append('a', 'Z') + return `\${r.url}: \${r.headers.get('a')}`; + }, 'http://nginx.org: X'], + ['inherit3', () => { + var h = new Headers(); + h.append('a', 'X'); + h.append('a', 'Z'); + var r = new Request("http://nginx.org", {headers: h}); + return `\${r.url}: \${r.headers.get('a')}`; + }, 'http://nginx.org: X, Z'], + ['content type', async () => { + var r = new Request("http://nginx.org", + {body: 'ABC', method: 'POST'}); + var body = await r.text(); + return `\${body}: \${r.headers.get('Content-Type')}`; + }, 'ABC: text/plain;charset=UTF-8'], + ['GET body', () => { + try { + var r = new Request("http://nginx.org", {body: 'ABC'}); + + } catch (e) { + if (!e.message.startsWith('Request body incompatible w')) { + throw e; + } + } + + return 'OK'; + + }, 'OK'], + ]; + + run(r, tests); + } + + async function response(r) { + const tests = [ + ['empty', async () => { + var r = new Response(); + var body = await r.text(); + return `\${r.url}: \${r.status} \${body} \${r.headers.get('a')}`; + }, ': 200 null'], + ['normal', async () => { + var r = new Response("ABC", {headers: {a: 'X', b: 'Y'}}); + var body = await r.text(); + return `\${r.url}: \${r.status} \${body} \${r.headers.get('a')}`; + }, ': 200 ABC X'], + ['headers', async () => { + var r = new Response(null, + {headers: new Headers({a: 'X', b: 'Y'})}); + var body = await r.text(); + return `\${r.url}: \${body} \${r.headers.get('b')}`; + }, ': Y'], + ['json', async () => { + var r = new Response('{"a": {"b": 42}}'); + var json = await r.json(); + return json.a.b; + }, 42], + ['statusText', () => { + const statuses = ['status text', 'aa\\u0000a']; + try { + statuses.forEach(s => { + var r = new Response(null, {statusText: s}); + if (r.statusText != s) { + throw new Error(`r.statusText != \${s}`); + } + }) + + } catch (e) { + if (!e.message.startsWith('invalid Response statusText')) { + throw e; + } + } + + return 'OK'; + + }, 'OK'], + ]; + + run(r, tests); + } + + async function fetch(r) { + const tests = [ + ['method', async () => { + var req = new Request("http://127.0.0.1:$p0/method", + {method: 'PUT'}); + var r = await ngx.fetch(req); + var body = await r.text(); + return `\${r.url}: \${r.status} \${body} \${r.headers.get('a')}`; + }, 'http://127.0.0.1:$p0/method: 200 PUT null'], + ['request body', async () => { + var req = new Request("http://127.0.0.1:$p0/body", + {body: 'foo'}); + var r = await ngx.fetch(req); + var body = await r.text(); + return `\${r.url}: \${r.status} \${body}`; + }, 'http://127.0.0.1:$p0/body: 201 foo'], + ['request body', async () => { + var h = new Headers({a: 'X'}); + h.append('a', 'Z'); + var req = new Request("http://127.0.0.1:$p0/header", + {headers: h}); + var r = await ngx.fetch(req); + var body = await r.text(); + return `\${r.url}: \${r.status} \${body}`; + }, 'http://127.0.0.1:$p0/header: 200 X, Z'], + ]; + + run(r, tests); + } + + export default {njs: test_njs, body, headers, request, response, fetch}; +EOF + +$t->try_run('no njs')->plan(4); + +############################################################################### + +local $TODO = 'not yet' unless has_version('0.7.10'); + +like(http_get('/headers'), qr/200 OK/s, 'headers tests'); +like(http_get('/request'), qr/200 OK/s, 'request tests'); +like(http_get('/response'), qr/200 OK/s, 'response tests'); +like(http_get('/fetch'), qr/200 OK/s, 'fetch tests'); + +############################################################################### + +sub has_version { + my $need = shift; + + http_get('/njs') =~ /^([.0-9]+)$/m; + + my @v = split(/\./, $1); + my ($n, $v); + + for $n (split(/\./, $need)) { + $v = shift @v || 0; + return 0 if $n > $v; + return 1 if $v > $n; + } + + return 1; +} + +###############################################################################