changeset 1814:1d88487eafbf

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.
author Dmitry Volyntsev <xeioex@nginx.com>
date Tue, 13 Dec 2022 08:50:42 -0800
parents 0a489a9abc7a
children 173c9b792c2c
files js_fetch.t js_fetch_objects.t
diffstat 2 files changed, 543 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- 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;
 
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', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function header(r) {
+        r.return(200, r.headersIn.a);
+    }
+
+    function body(r) {
+        r.return(201, r.requestText);
+    }
+
+    async function run(r, tests) {
+        var fails = [];
+        for (var i = 0; i < tests.length; i++) {
+            var v, t = tests[i];
+
+            try {
+                v = await t[1]();
+
+            } catch (e) {
+                v = e.message;
+            }
+
+            if (v != t[2]) {
+                fails.push(`\${t[0]}: got "\${v}" expected: "\${t[2]}"\n`);
+            }
+        }
+
+        r.return(fails.length ? 400 : 200, fails);
+    }
+
+    async function headers(r) {
+        const tests = [
+            ['empty', () => {
+                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;
+}
+
+###############################################################################