]> git.kaiwu.me - njs.git/commitdiff
Tests: imported nginx modules tests from nginx-tests.
authorDmitry Volyntsev <xeioex@nginx.com>
Tue, 23 May 2023 00:59:47 +0000 (17:59 -0700)
committerDmitry Volyntsev <xeioex@nginx.com>
Tue, 23 May 2023 00:59:47 +0000 (17:59 -0700)
47 files changed:
nginx/t/README [new file with mode: 0644]
nginx/t/js.t [new file with mode: 0644]
nginx/t/js_args.t [new file with mode: 0644]
nginx/t/js_async.t [new file with mode: 0644]
nginx/t/js_body_filter.t [new file with mode: 0644]
nginx/t/js_body_filter_if.t [new file with mode: 0644]
nginx/t/js_buffer.t [new file with mode: 0644]
nginx/t/js_dump.t [new file with mode: 0644]
nginx/t/js_fetch.t [new file with mode: 0644]
nginx/t/js_fetch_https.t [new file with mode: 0644]
nginx/t/js_fetch_objects.t [new file with mode: 0644]
nginx/t/js_fetch_resolver.t [new file with mode: 0644]
nginx/t/js_fetch_timeout.t [new file with mode: 0644]
nginx/t/js_fetch_verify.t [new file with mode: 0644]
nginx/t/js_header_filter.t [new file with mode: 0644]
nginx/t/js_header_filter_if.t [new file with mode: 0644]
nginx/t/js_headers.t [new file with mode: 0644]
nginx/t/js_import.t [new file with mode: 0644]
nginx/t/js_import2.t [new file with mode: 0644]
nginx/t/js_internal_redirect.t [new file with mode: 0644]
nginx/t/js_modules.t [new file with mode: 0644]
nginx/t/js_ngx.t [new file with mode: 0644]
nginx/t/js_object.t [new file with mode: 0644]
nginx/t/js_paths.t [new file with mode: 0644]
nginx/t/js_preload_object.t [new file with mode: 0644]
nginx/t/js_promise.t [new file with mode: 0644]
nginx/t/js_request_body.t [new file with mode: 0644]
nginx/t/js_return.t [new file with mode: 0644]
nginx/t/js_subrequests.t [new file with mode: 0644]
nginx/t/js_var.t [new file with mode: 0644]
nginx/t/js_var2.t [new file with mode: 0644]
nginx/t/js_variables.t [new file with mode: 0644]
nginx/t/stream_js.t [new file with mode: 0644]
nginx/t/stream_js_buffer.t [new file with mode: 0644]
nginx/t/stream_js_exit.t [new file with mode: 0644]
nginx/t/stream_js_fetch.t [new file with mode: 0644]
nginx/t/stream_js_fetch_https.t [new file with mode: 0644]
nginx/t/stream_js_fetch_init.t [new file with mode: 0644]
nginx/t/stream_js_import.t [new file with mode: 0644]
nginx/t/stream_js_import2.t [new file with mode: 0644]
nginx/t/stream_js_ngx.t [new file with mode: 0644]
nginx/t/stream_js_object.t [new file with mode: 0644]
nginx/t/stream_js_preload_object.t [new file with mode: 0644]
nginx/t/stream_js_send.t [new file with mode: 0644]
nginx/t/stream_js_var.t [new file with mode: 0644]
nginx/t/stream_js_var2.t [new file with mode: 0644]
nginx/t/stream_js_variables.t [new file with mode: 0644]

diff --git a/nginx/t/README b/nginx/t/README
new file mode 100644 (file)
index 0000000..ed1e6c5
--- /dev/null
@@ -0,0 +1,12 @@
+Test suite for nginx JavaScript module.
+
+This test suite relies on nginx-tests repository for the test library.
+
+Use prove to run tests as one usually do for perl tests.  Individual tests
+may be run as well.
+
+Usage:
+
+    $ TEST_NGINX_BINARY=/path/to/nginx prove -r -I /path/to/nginx-tests/lib/ nginx/t
+
+Refer to nginx-tests documentation for more details.
diff --git a/nginx/t/js.t b/nginx/t/js.t
new file mode 100644 (file)
index 0000000..5f2dcda
--- /dev/null
@@ -0,0 +1,391 @@
+#!/usr/bin/perl
+
+# (C) Roman Arutyunyan
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+use Socket qw/ CRLF /;
+
+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_set $test_method   test.method;
+    js_set $test_version  test.version;
+    js_set $test_addr     test.addr;
+    js_set $test_uri      test.uri;
+    js_set $test_var      test.variable;
+    js_set $test_type     test.type;
+    js_set $test_global   test.global_obj;
+    js_set $test_log      test.log;
+    js_set $test_internal test.sub_internal;
+    js_set $test_except   test.except;
+
+    js_import test.js;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /njs {
+            js_content test.njs;
+        }
+
+        location /method {
+            return 200 $test_method;
+        }
+
+        location /version {
+            return 200 $test_version;
+        }
+
+        location /addr {
+            return 200 $test_addr;
+        }
+
+        location /uri {
+            return 200 $test_uri;
+        }
+
+        location /var {
+            return 200 $test_var;
+        }
+
+        location /global {
+            return 200 $test_global;
+        }
+
+        location /body {
+            js_content test.request_body;
+        }
+
+        location /in_file {
+            client_body_in_file_only on;
+            js_content test.request_body;
+        }
+
+        location /status {
+            js_content test.status;
+        }
+
+        location /request_body {
+            js_content test.request_body;
+        }
+
+        location /request_body_cache {
+            js_content test.request_body_cache;
+        }
+
+        location /send {
+            js_content test.send;
+        }
+
+        location /return_method {
+            js_content test.return_method;
+        }
+
+        location /type {
+            js_content test.type;
+        }
+
+        location /log {
+            return 200 $test_log;
+        }
+
+        location /internal {
+            js_content test.internal;
+        }
+
+        location /sub_internal {
+            internal;
+            return 200 $test_internal;
+        }
+
+        location /except {
+            return 200 $test_except;
+        }
+
+        location /content_except {
+            js_content test.content_except;
+        }
+
+        location /content_empty {
+            js_content test.content_empty;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    var global = ['n', 'j', 's'].join("");
+
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function method(r) {
+        return 'method=' + r.method;
+    }
+
+    function version(r) {
+        return 'version=' + r.httpVersion;
+    }
+
+    function addr(r) {
+        return 'addr=' + r.remoteAddress;
+    }
+
+    function uri(r) {
+        return 'uri=' + r.uri;
+    }
+
+    function variable(r) {
+        return 'variable=' + r.variables.remote_addr;
+    }
+
+    function global_obj(r) {
+        return 'global=' + global;
+    }
+
+    function status(r) {
+        r.status = 204;
+        r.sendHeader();
+        r.finish();
+    }
+
+    function request_body(r) {
+        try {
+            var body = r.requestText;
+            r.return(200, body);
+
+        } catch (e) {
+            r.return(500, e.message);
+        }
+    }
+
+    function request_body_cache(r) {
+        function t(v) {return Buffer.isBuffer(v) ? 'buffer' : (typeof v);}
+        r.return(200,
+      `requestText:\${t(r.requestText)} requestBuffer:\${t(r.requestBuffer)}`);
+    }
+
+    function send(r) {
+        var a, s;
+        r.status = 200;
+        r.sendHeader();
+        for (a in r.args) {
+            if (a.substr(0, 3) == 'foo') {
+                s = r.args[a];
+                r.send('n=' + a + ', v=' + s.substr(0, 2) + ' ');
+            }
+        }
+        r.finish();
+    }
+
+    function return_method(r) {
+        r.return(Number(r.args.c), r.args.t);
+    }
+
+    function type(r) {
+        var p = r.args.path.split('.').reduce((a, v) => a[v], r);
+
+        var typ = Buffer.isBuffer(p) ? 'buffer' : (typeof p);
+        r.return(200, `type: \${typ}`);
+    }
+
+    function log(r) {
+        r.log('SEE-LOG');
+    }
+
+    async function internal(r) {
+        let reply = await r.subrequest('/sub_internal');
+
+        r.return(200, `parent: \${r.internal} sub: \${reply.responseText}`);
+    }
+
+    function sub_internal(r) {
+        return r.internal;
+    }
+
+    function except(r) {
+        var fs = require('fs');
+        fs.readFileSync();
+    }
+
+
+    function content_except(r) {
+        JSON.parse({}.a.a);
+    }
+
+    function content_empty(r) {
+    }
+
+    export default {njs:test_njs, method, version, addr, uri,
+                    variable, global_obj, status, request_body, internal,
+                    request_body_cache, send, return_method, sub_internal,
+                    type, log, except, content_except, content_empty};
+
+EOF
+
+$t->try_run('no njs available')->plan(27);
+
+###############################################################################
+
+like(http_get('/method'), qr/method=GET/, 'r.method');
+like(http_get('/version'), qr/version=1.0/, 'r.httpVersion');
+like(http_get('/addr'), qr/addr=127.0.0.1/, 'r.remoteAddress');
+like(http_get('/uri'), qr/uri=\/uri/, 'r.uri');
+
+like(http_get('/status'), qr/204 No Content/, 'r.status');
+
+like(http_post('/body'), qr/REQ-BODY/, 'request body');
+like(http_post('/in_file'), qr/request body is in a file/,
+       'request body in file');
+like(http_post_big('/body'), qr/200.*^(1234567890){1024}$/ms,
+       'request body big');
+
+like(http_get('/send?foo=12345&n=11&foo-2=bar&ndd=&foo-3=z'),
+       qr/n=foo, v=12 n=foo-2, v=ba n=foo-3, v=z/, 'r.send');
+
+like(http_get('/return_method?c=200'), qr/200 OK.*\x0d\x0a?\x0d\x0a?$/s,
+       'return code');
+like(http_get('/return_method?c=200&t=SEE-THIS'), qr/200 OK.*^SEE-THIS$/ms,
+       'return text');
+like(http_get('/return_method?c=301&t=path'), qr/ 301 .*Location: path/s,
+       'return redirect');
+like(http_get('/return_method?c=404'), qr/404 Not.*html/s, 'return error page');
+like(http_get('/return_method?c=inv'), qr/ 500 /, 'return invalid');
+
+like(http_get('/type?path=variables.host'), qr/200 OK.*type: string$/s,
+       'variables type');
+like(http_get('/type?path=rawVariables.host'), qr/200 OK.*type: buffer$/s,
+       'rawVariables type');
+
+like(http_post('/type?path=requestText'), qr/200 OK.*type: string$/s,
+       'requestText type');
+like(http_post('/type?path=requestBuffer'), qr/200 OK.*type: buffer$/s,
+       'requestBuffer type');
+like(http_post('/request_body_cache'),
+       qr/requestText:string requestBuffer:buffer$/s, 'request body cache');
+
+like(http_get('/var'), qr/variable=127.0.0.1/, 'r.variables');
+like(http_get('/global'), qr/global=njs/, 'global code');
+like(http_get('/log'), qr/200 OK/, 'r.log');
+
+TODO: {
+local $TODO = 'not yet' unless has_version('0.7.7');
+
+like(http_get('/internal'), qr/parent: false sub: true/, 'r.internal');
+
+}
+
+http_get('/except');
+http_get('/content_except');
+
+like(http_get('/content_empty'), qr/500 Internal Server Error/,
+       'empty handler');
+
+$t->stop();
+
+ok(index($t->read_file('error.log'), 'SEE-LOG') > 0, 'log js');
+ok(index($t->read_file('error.log'), 'at fs.readFileSync') > 0,
+       'js_set backtrace');
+ok(index($t->read_file('error.log'), 'at JSON.parse') > 0,
+       'js_content backtrace');
+
+###############################################################################
+
+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;
+}
+
+###############################################################################
+
+sub http_get_hdr {
+       my ($url, %extra) = @_;
+       return http(<<EOF, %extra);
+GET $url HTTP/1.0
+FoO: 12345
+
+EOF
+}
+
+sub http_get_ihdr {
+       my ($url, %extra) = @_;
+       return http(<<EOF, %extra);
+GET $url HTTP/1.0
+foo: 12345
+Host: localhost
+foo2: bar
+X-xxx: more
+foo-3: z
+
+EOF
+}
+
+sub http_post {
+       my ($url, %extra) = @_;
+
+       my $p = "POST $url HTTP/1.0" . CRLF .
+               "Host: localhost" . CRLF .
+               "Content-Length: 8" . CRLF .
+               CRLF .
+               "REQ-BODY";
+
+       return http($p, %extra);
+}
+
+sub http_post_big {
+       my ($url, %extra) = @_;
+
+       my $p = "POST $url HTTP/1.0" . CRLF .
+               "Host: localhost" . CRLF .
+               "Content-Length: 10240" . CRLF .
+               CRLF .
+               ("1234567890" x 1024);
+
+       return http($p, %extra);
+}
+
+###############################################################################
diff --git a/nginx/t/js_args.t b/nginx/t/js_args.t
new file mode 100644 (file)
index 0000000..bb5cf4f
--- /dev/null
@@ -0,0 +1,167 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, arguments tests.
+
+###############################################################################
+
+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;
+
+eval { require JSON::PP; };
+plan(skip_all => "JSON::PP not installed") if $@;
+
+my $t = Test::Nginx->new()->has(qw/http/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    js_import test.js;
+
+    js_set $test_iter     test.iter;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /njs {
+            js_content test.njs;
+        }
+
+        location /iter {
+            return 200 $test_iter;
+        }
+
+        location /keys {
+            js_content test.keys;
+        }
+
+        location /object {
+            js_content test.object;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function iter(r) {
+        var s = '', a;
+        for (a in r.args) {
+            if (a.substr(0, 3) == 'foo') {
+                s += r.args[a];
+            }
+        }
+
+        return s;
+    }
+
+    function keys(r) {
+        r.return(200, Object.keys(r.args).sort());
+    }
+
+    function object(r) {
+        r.return(200, JSON.stringify(r.args));
+    }
+
+    export default {njs: test_njs, iter, keys, object};
+
+EOF
+
+$t->try_run('no njs')->plan(15);
+
+###############################################################################
+
+sub recode {
+       my $json;
+       eval { $json = JSON::PP::decode_json(shift) };
+
+       if ($@) {
+               return "<failed to parse JSON>";
+       }
+
+       JSON::PP->new()->canonical()->encode($json);
+}
+
+sub get_json {
+       http_get(shift) =~ /\x0d\x0a?\x0d\x0a?(.*)/ms;
+       recode($1);
+}
+
+###############################################################################
+
+local $TODO = 'not yet' unless has_version('0.7.6');
+
+like(http_get('/iter?foo=12345&foo2=bar&nn=22&foo-3=z'), qr/12345barz/,
+       'r.args iteration');
+like(http_get('/iter?foo=123&foo2=&foo3&foo4=456'), qr/123456/,
+       'r.args iteration 2');
+like(http_get('/iter?foo=123&foo2=&foo3'), qr/123/, 'r.args iteration 3');
+like(http_get('/iter?foo=123&foo2='), qr/123/, 'r.args iteration 4');
+like(http_get('/iter?foo=1&foo=2'), qr/1,2/m, 'r.args iteration 5');
+
+like(http_get('/keys?b=1&c=2&a=5'), qr/a,b,c/m, 'r.args sorted keys');
+like(http_get('/keys?b=1&b=2'), qr/b/m, 'r.args duplicate keys');
+like(http_get('/keys?b=1&a&c='), qr/a,b,c/m, 'r.args empty value');
+
+is(get_json('/object'), '{}', 'empty object');
+is(get_json('/object?a=1&b=2&c=3'), '{"a":"1","b":"2","c":"3"}',
+       'ordinary object');
+is(get_json('/object?a=1&A=2'), '{"A":"2","a":"1"}',
+       'case sensitive object');
+is(get_json('/object?a=1&A=2&a=3'), '{"A":"2","a":["1","3"]}',
+       'duplicate keys object');
+is(get_json('/object?%61=1&a=2'), '{"a":["1","2"]}',
+       'keys percent-encoded object');
+is(get_json('/object?a=%62%63&b=%63%64'), '{"a":"bc","b":"cd"}',
+       'values percent-encoded object');
+is(get_json('/object?a=%6&b=%&c=%%&d=%zz'),
+       '{"a":"%6","b":"%","c":"%%","d":"%zz"}',
+       'values percent-encoded broken object');
+
+###############################################################################
+
+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;
+}
+
+###############################################################################
diff --git a/nginx/t/js_async.t b/nginx/t/js_async.t
new file mode 100644 (file)
index 0000000..73e7185
--- /dev/null
@@ -0,0 +1,225 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Async tests for http njs module.
+
+###############################################################################
+
+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_set $test_async      test.set_timeout;
+    js_set $context_var     test.context_var;
+    js_set $test_set_rv_var test.set_rv_var;
+
+    js_import test.js;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /njs {
+            js_content test.njs;
+        }
+
+        location /async_var {
+            return 200 $test_async;
+        }
+
+        location /shared_ctx {
+            add_header H $context_var;
+            js_content test.shared_ctx;
+        }
+
+        location /set_timeout {
+            js_content test.set_timeout;
+        }
+
+        location /set_timeout_many {
+            js_content test.set_timeout_many;
+        }
+
+        location /set_timeout_data {
+            postpone_output 0;
+            js_content test.set_timeout_data;
+        }
+
+        location /limit_rate {
+            postpone_output 0;
+            sendfile_max_chunk 5;
+            js_content test.limit_rate;
+        }
+
+        location /async_content {
+            js_content test.async_content;
+        }
+
+        location /set_rv_var {
+            return 200 $test_set_rv_var;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function set_timeout(r) {
+        var timerId = setTimeout(timeout_cb_r, 5, r, 0);
+        clearTimeout(timerId);
+        setTimeout(timeout_cb_r, 5, r, 0)
+    }
+
+    function set_timeout_data(r) {
+        setTimeout(timeout_cb_data, 5, r, 0);
+    }
+
+    function set_timeout_many(r) {
+        for (var i = 0; i < 5; i++) {
+            setTimeout(timeout_cb_empty, 5, r, i);
+        }
+
+        setTimeout(timeout_cb_reply, 10, r);
+    }
+
+    function timeout_cb_r(r, cnt) {
+        if (cnt == 10) {
+            r.status = 200;
+            r.headersOut['Content-Type'] = 'foo';
+            r.sendHeader();
+            r.finish();
+
+        } else {
+            setTimeout(timeout_cb_r, 5, r, ++cnt);
+        }
+    }
+
+    function timeout_cb_empty(r, arg) {
+        r.log("timeout_cb_empty" + arg);
+    }
+
+    function timeout_cb_reply(r) {
+        r.status = 200;
+        r.headersOut['Content-Type'] = 'reply';
+        r.sendHeader();
+        r.finish();
+    }
+
+    function timeout_cb_data(r, counter) {
+        if (counter == 0) {
+            r.log("timeout_cb_data: init");
+            r.status = 200;
+            r.sendHeader();
+            setTimeout(timeout_cb_data, 5, r, ++counter);
+
+        } else if (counter == 10) {
+            r.log("timeout_cb_data: finish");
+            r.finish();
+
+        } else {
+            r.send("" + counter);
+            setTimeout(timeout_cb_data, 5, r, ++counter);
+        }
+    }
+
+    var js_;
+    function context_var() {
+        return js_;
+    }
+
+    function shared_ctx(r) {
+        js_ = r.variables.arg_a;
+
+        r.status = 200;
+        r.sendHeader();
+        r.finish();
+    }
+
+    function limit_rate_cb(r) {
+        r.finish();
+    }
+
+    function limit_rate(r) {
+        r.status = 200;
+        r.sendHeader();
+        r.send("AAAAA".repeat(10))
+        setTimeout(limit_rate_cb, 1000, r);
+    }
+
+    function pr(x) {
+        return new Promise(resolve => {resolve(x)}).then(v => v).then(v => v);
+    }
+
+    async function async_content(r) {
+        const a1 = await pr('A');
+        const a2 = await pr('B');
+
+        r.return(200, `retval: \${a1 + a2}`);
+    }
+
+    async function set_rv_var(r) {
+        const a1 = await pr(10);
+        const a2 = await pr(20);
+
+        r.setReturnValue(`retval: \${a1 + a2}`);
+    }
+
+    export default {njs:test_njs, set_timeout, set_timeout_data,
+                    set_timeout_many, context_var, shared_ctx, limit_rate,
+                    async_content, set_rv_var};
+
+EOF
+
+$t->try_run('no njs available')->plan(9);
+
+###############################################################################
+
+like(http_get('/set_timeout'), qr/Content-Type: foo/, 'setTimeout');
+like(http_get('/set_timeout_many'), qr/Content-Type: reply/, 'setTimeout many');
+like(http_get('/set_timeout_data'), qr/123456789/, 'setTimeout data');
+like(http_get('/shared_ctx?a=xxx'), qr/H: xxx/, 'shared context');
+like(http_get('/limit_rate'), qr/A{50}/, 'limit_rate');
+
+like(http_get('/async_content'), qr/retval: AB/, 'async content');
+like(http_get('/set_rv_var'), qr/retval: 30/, 'set return value variable');
+
+http_get('/async_var');
+
+$t->stop();
+
+ok(index($t->read_file('error.log'), 'pending events') > 0,
+   'pending js events');
+ok(index($t->read_file('error.log'), 'async operation inside') > 0,
+   'async op in var handler');
+
+###############################################################################
diff --git a/nginx/t/js_body_filter.t b/nginx/t/js_body_filter.t
new file mode 100644 (file)
index 0000000..b90a250
--- /dev/null
@@ -0,0 +1,168 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, body filter.
+
+###############################################################################
+
+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 proxy/)
+       ->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 /append {
+            js_body_filter test.append;
+            proxy_pass http://127.0.0.1:8081/source;
+        }
+
+        location /buffer_type {
+            js_body_filter test.buffer_type buffer_type=buffer;
+            proxy_pass http://127.0.0.1:8081/source;
+        }
+
+        location /forward {
+            js_body_filter test.forward buffer_type=string;
+            proxy_pass http://127.0.0.1:8081/source;
+        }
+
+        location /filter {
+            proxy_buffering off;
+            js_body_filter test.filter;
+            proxy_pass http://127.0.0.1:8081/source;
+        }
+
+        location /prepend {
+            js_body_filter test.prepend;
+            proxy_pass http://127.0.0.1:8081/source;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8081;
+        server_name  localhost;
+
+        location /source {
+            postpone_output 1;
+            js_content test.source;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function append(r, data, flags) {
+        r.sendBuffer(data, {last:false});
+
+        if (flags.last) {
+            r.sendBuffer("XXX", flags);
+        }
+    }
+
+    var collect = Buffer.from([]);
+    function buffer_type(r, data, flags) {
+        collect = Buffer.concat([collect, data]);
+
+        if (flags.last) {
+            r.sendBuffer(collect, flags);
+        }
+    }
+
+    function chain(chunks, i) {
+        if (i < chunks.length) {
+            chunks.r.send(chunks[i++]);
+            setTimeout(chunks.chain, chunks.delay, chunks, i);
+
+        } else {
+            chunks.r.finish();
+        }
+    }
+
+    function source(r) {
+        var chunks = ['AAA', 'BB', 'C', 'DDDD'];
+        chunks.delay = 5;
+        chunks.r = r;
+        chunks.chain = chain;
+
+        r.status = 200;
+        r.sendHeader();
+        chain(chunks, 0);
+    }
+
+    function filter(r, data, flags) {
+        if (flags.last || data.length >= Number(r.args.len)) {
+            r.sendBuffer(`\${data}|`, flags);
+
+            if (r.args.dup && !flags.last) {
+                r.sendBuffer(data, flags);
+            }
+        }
+    }
+
+    function forward(r, data, flags) {
+        r.sendBuffer(data, flags);
+    }
+
+    function prepend(r, data, flags) {
+        r.sendBuffer("XXX");
+        r.sendBuffer(data, flags);
+        r.done();
+    }
+
+    export default {njs: test_njs, append, buffer_type, filter, forward,
+                    prepend, source};
+
+EOF
+
+$t->try_run('no njs body filter')->plan(6);
+
+###############################################################################
+
+like(http_get('/append'), qr/AAABBCDDDDXXX/, 'append');
+like(http_get('/buffer_type'), qr/AAABBCDDDD/, 'buffer type');
+like(http_get('/forward'), qr/AAABBCDDDD/, 'forward');
+like(http_get('/filter?len=3'), qr/AAA|DDDD|/, 'filter 3');
+like(http_get('/filter?len=2&dup=1'), qr/AAA|AAABB|BBDDDD|DDDD/,
+       'filter 2 dup');
+like(http_get('/prepend'), qr/XXXAAABBCDDDD/, 'prepend');
+
+###############################################################################
diff --git a/nginx/t/js_body_filter_if.t b/nginx/t/js_body_filter_if.t
new file mode 100644 (file)
index 0000000..af0aa86
--- /dev/null
@@ -0,0 +1,127 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, body filter, if context.
+
+###############################################################################
+
+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 proxy 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 /filter {
+            if ($arg_name ~ "prepend") {
+                js_body_filter test.prepend;
+            }
+
+            if ($arg_name ~ "append") {
+                js_body_filter test.append;
+            }
+
+            js_body_filter test.should_not_be_called;
+
+            proxy_pass http://127.0.0.1:8081/source;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8081;
+        server_name  localhost;
+
+        location /source {
+            postpone_output 1;
+            js_content test.source;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function append(r, data, flags) {
+        r.sendBuffer(data, {last:false});
+
+        if (flags.last) {
+            r.sendBuffer("XXX", flags);
+        }
+    }
+
+    function chain(chunks, i) {
+        if (i < chunks.length) {
+            chunks.r.send(chunks[i++]);
+            setTimeout(chunks.chain, chunks.delay, chunks, i);
+
+        } else {
+            chunks.r.finish();
+        }
+    }
+
+    function source(r) {
+        var chunks = ['AAA', 'BB', 'C', 'DDDD'];
+        chunks.delay = 5;
+        chunks.r = r;
+        chunks.chain = chain;
+
+        r.status = 200;
+        r.sendHeader();
+        chain(chunks, 0);
+    }
+
+    function prepend(r, data, flags) {
+        r.sendBuffer("XXX");
+        r.sendBuffer(data, flags);
+        r.done();
+    }
+
+    export default {njs: test_njs, append, prepend, source};
+
+EOF
+
+$t->try_run('no njs body filter')->plan(2);
+
+###############################################################################
+
+like(http_get('/filter?name=append'), qr/AAABBCDDDDXXX/, 'append');
+like(http_get('/filter?name=prepend'), qr/XXXAAABBCDDDD/, 'prepend');
+
+###############################################################################
diff --git a/nginx/t/js_buffer.t b/nginx/t/js_buffer.t
new file mode 100644 (file)
index 0000000..ce4c15d
--- /dev/null
@@ -0,0 +1,184 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, buffer properties.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+use Socket qw/ CRLF /;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+eval { require JSON::PP; };
+plan(skip_all => "JSON::PP not installed") if $@;
+
+my $t = Test::Nginx->new()->has(qw/http rewrite proxy/)
+       ->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 /return {
+            js_content test.return;
+        }
+
+        location /req_body {
+            js_content test.req_body;
+        }
+
+        location /res_body {
+            js_content test.res_body;
+        }
+
+        location /res_text {
+            js_content test.res_text;
+        }
+
+        location /binary_var {
+            js_content test.binary_var;
+        }
+
+        location /p/ {
+            proxy_pass http://127.0.0.1:8081/;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8081;
+        server_name  localhost;
+
+        location /sub1 {
+            return 200 '{"a": {"b": 1}}';
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function test_return(r) {
+        var body = Buffer.from("body: ");
+        body = Buffer.concat([body, Buffer.from(r.args.text)]);
+        r.return(200, body);
+    }
+
+    function req_body(r) {
+        var body = r.requestBuffer;
+        var view = new DataView(body.buffer);
+        view.setInt8(2, 'c'.charCodeAt(0));
+        r.return(200, JSON.parse(body).c.b);
+    }
+
+    function type(v) {return Buffer.isBuffer(v) ? 'buffer' : (typeof v);}
+
+    function res_body(r) {
+        r.subrequest('/p/sub1')
+        .then(reply => {
+            var body = reply.responseBuffer;
+            var view = new DataView(body.buffer);
+            view.setInt8(2, 'c'.charCodeAt(0));
+            body = JSON.parse(body);
+            body.type = type(reply.responseBuffer);
+            r.return(200, JSON.stringify(body));
+        })
+    }
+
+    function res_text(r) {
+        r.subrequest('/p/sub1')
+        .then(reply => {
+            var body = JSON.parse(reply.responseText);
+            body.type = type(reply.responseText);
+            r.return(200, JSON.stringify(body));
+        })
+    }
+
+    function binary_var(r) {
+        var test = r.rawVariables.binary_remote_addr
+                   .equals(Buffer.from([127,0,0,1]));
+        r.return(200, test);
+    }
+
+    export default {njs: test_njs, return: test_return, req_body, res_body,
+                    res_text, binary_var};
+
+EOF
+
+$t->try_run('no njs buffer')->plan(5);
+
+###############################################################################
+
+like(http_get('/return?text=FOO'), qr/200 OK.*body: FOO$/s,
+       'return buffer');
+like(http_post('/req_body'), qr/200 OK.*BAR$/s, 'request buffer');
+is(get_json('/res_body'), '{"c":{"b":1},"type":"buffer"}', 'response buffer');
+is(get_json('/res_text'), '{"a":{"b":1},"type":"string"}', 'response text');
+like(http_get('/binary_var'), qr/200 OK.*true$/s,
+       'binary var');
+
+###############################################################################
+
+sub recode {
+       my $json;
+       eval { $json = JSON::PP::decode_json(shift) };
+
+       if ($@) {
+               return "<failed to parse JSON>";
+       }
+
+       JSON::PP->new()->canonical()->encode($json);
+}
+
+sub get_json {
+       http_get(shift) =~ /\x0d\x0a?\x0d\x0a?(.*)/ms;
+       recode($1);
+}
+
+sub http_post {
+       my ($url, %extra) = @_;
+
+       my $p = "POST $url HTTP/1.0" . CRLF .
+               "Host: localhost" . CRLF .
+               "Content-Length: 17" . CRLF .
+               CRLF .
+               "{\"a\":{\"b\":\"BAR\"}}";
+
+       return http($p, %extra);
+}
+
+###############################################################################
diff --git a/nginx/t/js_dump.t b/nginx/t/js_dump.t
new file mode 100644 (file)
index 0000000..c00a53a
--- /dev/null
@@ -0,0 +1,110 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, request object dump.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+use Socket qw/ CRLF /;
+
+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 /dump {
+            js_content test.dump;
+        }
+
+        location /stringify {
+            js_content test.stringify;
+        }
+
+        location /stringify_subrequest {
+            js_content test.stringify_subrequest;
+        }
+
+        location /js_sub {
+            return 201 '{$request_method}';
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function dump(r) {
+        r.headersOut.baz = 'bar';
+        r.return(200, njs.dump(r));
+    }
+
+    function stringify(r) {
+        r.headersOut.baz = 'bar';
+        var obj = JSON.parse(JSON.stringify(r));
+        r.return(200, JSON.stringify(obj));
+    }
+
+    function stringify_subrequest(r) {
+        r.subrequest('/js_sub', reply => {
+            r.return(200, JSON.stringify(reply))
+        });
+    }
+
+    export default {dump, stringify, stringify_subrequest};
+
+EOF
+
+$t->try_run('no njs dump')->plan(3);
+
+###############################################################################
+
+like(http(
+       'GET /dump?v=1&t=x HTTP/1.0' . CRLF
+       . 'Foo: bar' . CRLF
+       . 'Foo2: bar2' . CRLF
+       . 'Host: localhost' . CRLF . CRLF
+), qr/method:'GET'/, 'njs.dump(r)');
+
+like(http(
+       'GET /stringify?v=1&t=x HTTP/1.0' . CRLF
+       . 'Foo: bar' . CRLF
+       . 'Foo2: bar2' . CRLF
+       . 'Host: localhost' . CRLF . CRLF
+), qr/headersOut":\{"baz":"bar"}/, 'JSON.stringify(r)');
+
+like(http(
+       'GET /stringify_subrequest HTTP/1.0' . CRLF
+       . 'Host: localhost' . CRLF . CRLF
+), qr/"status":201/, 'JSON.stringify(reply)');
+
+###############################################################################
diff --git a/nginx/t/js_fetch.t b/nginx/t/js_fetch.t
new file mode 100644 (file)
index 0000000..428be90
--- /dev/null
@@ -0,0 +1,710 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, fetch method.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+use Socket qw/ CRLF /;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+eval { require JSON::PP; };
+plan(skip_all => "JSON::PP not installed") if $@;
+
+my $t = Test::Nginx->new()->has(qw/http/)
+       ->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 /broken {
+            js_content test.broken;
+        }
+
+        location /broken_response {
+            js_content test.broken_response;
+        }
+
+        location /body {
+            js_content test.body;
+        }
+
+        location /body_special {
+            js_content test.body_special;
+        }
+
+        location /chain {
+            js_content test.chain;
+        }
+
+        location /chunked_ok {
+            js_content test.chunked_ok;
+        }
+
+        location /chunked_fail {
+            js_content test.chunked_fail;
+        }
+
+        location /header {
+            js_content test.header;
+        }
+
+        location /host_header {
+            js_content test.host_header;
+        }
+
+        location /header_iter {
+            js_content test.header_iter;
+        }
+
+        location /multi {
+            js_content test.multi;
+        }
+
+        location /property {
+            js_content test.property;
+        }
+
+        location /loc {
+            js_content test.loc;
+        }
+
+        location /json { }
+    }
+
+    server {
+        listen       127.0.0.1:8081;
+        server_name  localhost;
+
+        location /loc {
+            js_content test.loc;
+        }
+
+        location /host {
+            return 200 $http_host;
+        }
+    }
+}
+
+EOF
+
+my $p0 = port(8080);
+my $p1 = port(8081);
+my $p2 = port(8082);
+
+$t->write_file('json', '{"a":[1,2], "b":{"c":"FIELD"}}');
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function body(r) {
+        var loc = r.args.loc;
+        var getter = r.args.getter;
+
+        function query(obj) {
+            var path = r.args.path;
+            var retval = (getter == 'arrayBuffer') ? Buffer.from(obj).toString()
+                                                   : obj;
+
+            if (path) {
+                retval = path.split('.').reduce((a, v) => a[v], obj);
+            }
+
+            return JSON.stringify(retval);
+        }
+
+        ngx.fetch(`http://127.0.0.1:$p0/\${loc}`)
+        .then(reply => reply[getter]())
+        .then(data => r.return(200, query(data)))
+        .catch(e => r.return(501, e.message))
+    }
+
+    function property(r) {
+        var opts = {headers:{}};
+
+        if (r.args.code) {
+            opts.headers.code = r.args.code;
+        }
+
+        var p = ngx.fetch('http://127.0.0.1:$p0/loc', opts)
+
+        if (r.args.readBody) {
+            p = p.then(rep =>
+                 rep.text().then(body => {rep.text = body; return rep;}))
+        }
+
+        p.then(reply => r.return(200, reply[r.args.pr]))
+        .catch(e => r.return(501, e.message))
+    }
+
+    function process_errors(r, tests) {
+        var results = [];
+
+        tests.forEach(args => {
+            ngx.fetch.apply(r, args)
+            .then(reply => {
+                r.return(400, '["unexpected then"]');
+            })
+            .catch(e => {
+                results.push(e.message);
+
+                if (results.length == tests.length) {
+                    results.sort();
+                    r.return(200, JSON.stringify(results));
+                }
+            })
+        })
+    }
+
+    function broken(r) {
+        var tests = [
+            ['http://127.0.0.1:1/loc'],
+            ['http://127.0.0.1:80800/loc'],
+            [Symbol.toStringTag],
+        ];
+
+        return process_errors(r, tests);
+    }
+
+    function broken_response(r) {
+        var tests = [
+            ['http://127.0.0.1:$p2/status_line'],
+            ['http://127.0.0.1:$p2/length'],
+            ['http://127.0.0.1:$p2/header'],
+            ['http://127.0.0.1:$p2/headers'],
+            ['http://127.0.0.1:$p2/content_length'],
+        ];
+
+        return process_errors(r, tests);
+    }
+
+    function chain(r) {
+        var results = [];
+        var reqs = [
+             ['http://127.0.0.1:$p0/loc'],
+             ['http://127.0.0.1:$p1/loc'],
+           ];
+
+           function next(reply) {
+              if (reqs.length == 0) {
+                 r.return(200, "SUCCESS");
+                 return;
+              }
+
+              ngx.fetch.apply(r, reqs.pop())
+              .then(next)
+              .catch(e => r.return(400, e.message))
+           }
+
+           next();
+    }
+
+    function chunked_ok(r) {
+        var results = [];
+        var tests = [
+            ['http://127.0.0.1:$p2/big/ok', {max_response_body_size:128000}],
+            ['http://127.0.0.1:$p2/chunked/ok'],
+            ['http://127.0.0.1:$p2/chunked/big'],
+        ];
+
+        function collect(v) {
+            results.push(v);
+
+            if (results.length == tests.length) {
+                r.return(200);
+            }
+        }
+
+        tests.forEach(args => {
+            ngx.fetch.apply(r, args)
+            .then(reply => reply.text())
+            .then(body => collect(body.length))
+        })
+    }
+
+    function chunked_fail(r) {
+        var results = [];
+        var tests = [
+            ['http://127.0.0.1:$p2/big', {max_response_body_size:128000}],
+            ['http://127.0.0.1:$p2/chunked'],
+            ['http://127.0.0.1:$p2/chunked/big', {max_response_body_size:128}],
+        ];
+
+        function collect(v) {
+            results.push(v);
+
+            if (results.length == tests.length) {
+                r.return(200);
+            }
+        }
+
+        tests.forEach(args => {
+            ngx.fetch.apply(r, args)
+            .then(reply => reply.text())
+            .catch(e => collect(e.message))
+        })
+    }
+
+    function header(r) {
+        var url = `http://127.0.0.1:$p2/\${r.args.loc}`;
+        var method = r.args.method ? r.args.method : 'get';
+
+        var p = ngx.fetch(url)
+
+        if (r.args.readBody) {
+            p = p.then(rep =>
+                 rep.text().then(body => {rep.text = body; return rep;}))
+        }
+
+        p.then(reply => {
+            var h = reply.headers[method](r.args.h);
+            r.return(200, njs.dump(h));
+        })
+        .catch(e => r.return(501, e.message))
+    }
+
+    async function host_header(r) {
+        const reply = await ngx.fetch(`http://127.0.0.1:$p1/host`,
+                                      {headers: {Host: r.args.host}});
+        const body = await reply.text();
+        r.return(200, body);
+    }
+
+    async function body_special(r) {
+        let opts = {};
+
+        if (r.args.method) {
+            opts.method = r.args.method;
+        }
+
+        let reply = await ngx.fetch(`http://127.0.0.1:$p2/\${r.args.loc}`,
+                                    opts);
+        let body = await reply.text();
+
+        r.return(200, body != '' ? body : '<empty>');
+    }
+
+    async function header_iter(r) {
+        let url = `http://127.0.0.1:$p2/\${r.args.loc}`;
+
+        let response = await ngx.fetch(url);
+
+        let headers = response.headers;
+        let out = [];
+        for (let key in response.headers) {
+            if (key != 'Connection') {
+                out.push(`\${key}:\${headers.get(key)}`);
+            }
+        }
+
+        r.return(200, njs.dump(out));
+    }
+
+    function multi(r) {
+        var results = [];
+        var tests = [
+             [
+              'http://127.0.0.1:$p0/loc',
+               { headers: {Code: 201}},
+             ],
+             [
+              'http://127.0.0.1:$p0/loc',
+               { method:'POST', headers: {Code: 401}, body: 'OK'},
+             ],
+             [
+              'http://127.0.0.1:$p1/loc',
+               { method:'PATCH',
+                 headers: {bar:'xxx'}},
+             ],
+           ];
+
+        function cmp(a,b) {
+            if (a.b > b.b) {return 1;}
+            if (a.b < b.b) {return -1;}
+            return 0
+        }
+
+        tests.forEach(args => {
+            ngx.fetch.apply(r, args)
+            .then(rep =>
+                 rep.text().then(body => {rep.text = body; return rep;}))
+            .then(rep => {
+                results.push({b:rep.text,
+                              c:rep.status,
+                              u:rep.url});
+
+                if (results.length == tests.length) {
+                    results.sort(cmp);
+                    r.return(200, JSON.stringify(results));
+                }
+            })
+            .catch(e => {
+                r.return(400, `["\${e.message}"]`);
+                throw e;
+            })
+        })
+
+        if (r.args.throw) {
+            throw 'Oops';
+        }
+    }
+
+    function str(v) { return v ? v : ''};
+
+    function loc(r) {
+        var v = r.variables;
+        var body = str(r.requestText);
+        var bar = str(r.headersIn.bar);
+        var c = r.headersIn.code ? Number(r.headersIn.code) : 200;
+        r.return(c, `\${v.request_method}:\${bar}:\${body}`);
+    }
+
+     export default {njs: test_njs, body, broken, broken_response, body_special,
+                     chain, chunked_ok, chunked_fail, header, header_iter,
+                     host_header, multi, loc, property};
+EOF
+
+$t->try_run('no njs.fetch')->plan(35);
+
+$t->run_daemon(\&http_daemon, port(8082));
+$t->waitforsocket('127.0.0.1:' . port(8082));
+
+###############################################################################
+
+like(http_get('/body?getter=arrayBuffer&loc=loc'), qr/200 OK.*"GET::"$/s,
+       'fetch body arrayBuffer');
+like(http_get('/body?getter=text&loc=loc'), qr/200 OK.*"GET::"$/s,
+       'fetch body text');
+like(http_get('/body?getter=json&loc=json&path=b.c'),
+       qr/200 OK.*"FIELD"$/s, 'fetch body json');
+like(http_get('/body?getter=json&loc=loc'), qr/501/s,
+       'fetch body json invalid');
+like(http_get('/body_special?loc=parted'), qr/200 OK.*X{32000}$/s,
+       'fetch body parted');
+like(http_get('/property?pr=bodyUsed'), qr/false$/s,
+       'fetch bodyUsed false');
+like(http_get('/property?pr=bodyUsed&readBody=1'), qr/true$/s,
+       'fetch bodyUsed true');
+like(http_get('/property?pr=ok'), qr/200 OK.*true$/s,
+       'fetch ok true');
+like(http_get('/property?pr=ok&code=401'), qr/200 OK.*false$/s,
+       'fetch ok false');
+like(http_get('/property?pr=redirected'), qr/200 OK.*false$/s,
+       'fetch redirected false');
+like(http_get('/property?pr=statusText'), qr/200 OK.*OK$/s,
+       'fetch statusText OK');
+like(http_get('/property?pr=statusText&code=403'), qr/200 OK.*Forbidden$/s,
+       'fetch statusText Forbidden');
+like(http_get('/property?pr=type'), qr/200 OK.*basic$/s,
+       'fetch type');
+like(http_get('/header?loc=duplicate_header&h=BAR'), qr/200 OK.*c$/s,
+       '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,
+       'fetch header duplicate');
+like(http_get('/header?loc=duplicate_header&h=BAR&method=getAll'),
+       qr/200 OK.*\['c']$/s, 'fetch getAll header');
+like(http_get('/header?loc=duplicate_header&h=BARR&method=getAll'),
+       qr/200 OK.*\[]$/s, 'fetch getAll no header');
+like(http_get('/header?loc=duplicate_header&h=FOO&method=getAll'),
+       qr/200 OK.*\['a','b']$/s, 'fetch getAll duplicate');
+like(http_get('/header?loc=duplicate_header&h=bar&method=has'),
+       qr/200 OK.*true$/s, 'fetch header has');
+like(http_get('/header?loc=duplicate_header&h=buz&method=has'),
+       qr/200 OK.*false$/s, 'fetch header does not have');
+like(http_get('/header?loc=chunked/big&h=BAR&readBody=1'), qr/200 OK.*xxx$/s,
+       'fetch chunked header');
+is(get_json('/multi'),
+       '[{"b":"GET::","c":201,"u":"http://127.0.0.1:'.$p0.'/loc"},' .
+       '{"b":"PATCH:xxx:","c":200,"u":"http://127.0.0.1:'.$p1.'/loc"},' .
+       '{"b":"POST::OK","c":401,"u":"http://127.0.0.1:'.$p0.'/loc"}]',
+       'fetch multi');
+like(http_get('/multi?throw=1'), qr/500/s, 'fetch destructor');
+like(http_get('/broken'), qr/200/s, 'fetch broken');
+like(http_get('/broken_response'), qr/200/s, 'fetch broken response');
+like(http_get('/chunked_ok'), qr/200/s, 'fetch chunked ok');
+like(http_get('/chunked_fail'), qr/200/s, 'fetch chunked fail');
+like(http_get('/chain'), qr/200 OK.*SUCCESS$/s, 'fetch chain');
+
+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','Moo:a, ?b']$/s,
+       'fetch header duplicate large');
+
+TODO: {
+local $TODO = 'not yet' unless has_version('0.7.7');
+
+like(http_get('/body_special?loc=no_content_length'),
+       qr/200 OK.*CONTENT-BODY$/s, 'fetch body without content-length');
+like(http_get('/body_special?loc=no_content_length/parted'),
+       qr/200 OK.*X{32000}$/s, 'fetch body without content-length parted');
+
+}
+
+TODO: {
+local $TODO = 'not yet' unless has_version('0.7.8');
+
+like(http_get('/body_special?loc=head&method=HEAD'),
+       qr/200 OK.*<empty>$/s, 'fetch head method');
+like(http_get('/body_special?loc=length&method=head'),
+       qr/200 OK.*<empty>$/s, 'fetch head method lower case');
+
+}
+
+TODO: {
+local $TODO = 'not yet' unless has_version('0.8.0');
+
+like(http_get('/host_header?host=FOOBAR'), qr/200 OK.*FOOBAR$/s,
+       'fetch host header');
+}
+
+###############################################################################
+
+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;
+}
+
+###############################################################################
+
+sub recode {
+       my $json;
+       eval { $json = JSON::PP::decode_json(shift) };
+
+       if ($@) {
+               return "<failed to parse JSON>";
+       }
+
+       JSON::PP->new()->canonical()->encode($json);
+}
+
+sub get_json {
+       http_get(shift) =~ /\x0d\x0a?\x0d\x0a?(.*)/ms;
+       recode($1);
+}
+
+###############################################################################
+
+sub http_daemon {
+       my $port = shift;
+
+       my $server = IO::Socket::INET->new(
+               Proto => 'tcp',
+               LocalAddr => '127.0.0.1:' . $port,
+               Listen => 5,
+               Reuse => 1
+       ) or die "Can't create listening socket: $!\n";
+
+       local $SIG{PIPE} = 'IGNORE';
+
+       while (my $client = $server->accept()) {
+               $client->autoflush(1);
+
+               my $headers = '';
+               my $uri = '';
+
+               while (<$client>) {
+                       $headers .= $_;
+                       last if (/^\x0d?\x0a?$/);
+               }
+
+               $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i;
+
+               if ($uri eq '/status_line') {
+                       print $client
+                               "HTTP/1.1 2A";
+
+               } elsif ($uri eq '/content_length') {
+                       print $client
+                               "HTTP/1.1 200 OK" . CRLF .
+                               "Content-Length: " . CRLF .
+                               "Connection: close" . CRLF .
+                               CRLF;
+
+               } elsif ($uri eq '/header') {
+                       print $client
+                               "HTTP/1.1 200 OK" . CRLF .
+                               "@#" . CRLF .
+                               "Connection: close" . CRLF .
+                               CRLF;
+
+               } elsif ($uri eq '/duplicate_header') {
+                       print $client
+                               "HTTP/1.1 200 OK" . CRLF .
+                               "Foo: a" . CRLF .
+                               "bar: c" . CRLF .
+                               "Foo: b" . CRLF .
+                               "Connection: close" . CRLF .
+                               CRLF;
+
+               } elsif ($uri eq '/duplicate_header_large') {
+                       print $client
+                               "HTTP/1.1 200 OK" . CRLF .
+                               "A: a" . CRLF .
+                               "B: a" . CRLF .
+                               "C: a" . CRLF .
+                               "D: a" . CRLF .
+                               "E: a" . CRLF .
+                               "F: a" . CRLF .
+                               "G: a" . CRLF .
+                               "H: a" . CRLF .
+                               "Moo: a" . CRLF .
+                               "Moo: b" . CRLF .
+                               "Connection: close" . CRLF .
+                               CRLF;
+
+               } elsif ($uri eq '/headers') {
+                       print $client
+                               "HTTP/1.1 200 OK" . CRLF .
+                               "Connection: close" . CRLF;
+
+               } elsif ($uri eq '/length') {
+                       print $client
+                               "HTTP/1.1 200 OK" . CRLF .
+                               "Content-Length: 100" . CRLF .
+                               "Connection: close" . CRLF .
+                               CRLF .
+                               "unfinished" . CRLF;
+
+               } elsif ($uri eq '/head') {
+                       print $client
+                               "HTTP/1.1 200 OK" . CRLF .
+                               "Content-Length: 100" . CRLF .
+                               "Connection: close" . CRLF .
+                               CRLF;
+
+               } elsif ($uri eq '/parted') {
+                       print $client
+                               "HTTP/1.1 200 OK" . CRLF .
+                               "Content-Length: 32000" . CRLF .
+                               "Connection: close" . CRLF .
+                               CRLF;
+
+                       for (1 .. 4) {
+                               select undef, undef, undef, 0.01;
+                               print $client "X" x 8000;
+                       }
+
+               } elsif ($uri eq '/no_content_length') {
+                       print $client
+                               "HTTP/1.1 200 OK" . CRLF .
+                               "Connection: close" . CRLF .
+                               CRLF .
+                               "CONTENT-BODY";
+
+               } elsif ($uri eq '/no_content_length/parted') {
+                       print $client
+                               "HTTP/1.1 200 OK" . CRLF .
+                               "Connection: close" . CRLF .
+                               CRLF;
+
+                       for (1 .. 4) {
+                               select undef, undef, undef, 0.01;
+                               print $client "X" x 8000;
+                       }
+
+               } elsif ($uri eq '/big') {
+                       print $client
+                               "HTTP/1.1 200 OK" . CRLF .
+                               "Content-Length: 100100" . CRLF .
+                               "Connection: close" . CRLF .
+                               CRLF;
+                       for (1 .. 1000) {
+                               print $client ("X" x 98) . CRLF;
+                       }
+                       print $client "unfinished" . CRLF;
+
+               } elsif ($uri eq '/big/ok') {
+                       print $client
+                               "HTTP/1.1 200 OK" . CRLF .
+                               "Content-Length: 100010" . CRLF .
+                               "Connection: close" . CRLF .
+                               CRLF;
+                       for (1 .. 1000) {
+                               print $client ("X" x 98) . CRLF;
+                       }
+                       print $client "finished" . CRLF;
+
+               } elsif ($uri eq '/chunked') {
+                       print $client
+                               "HTTP/1.1 200 OK" . CRLF .
+                               "Transfer-Encoding: chunked" . CRLF .
+                               "Connection: close" . CRLF .
+                               CRLF .
+                               "ff" . CRLF .
+                               "unfinished" . CRLF;
+
+               } elsif ($uri eq '/chunked/ok') {
+                       print $client
+                               "HTTP/1.1 200 OK" . CRLF .
+                               "Transfer-Encoding: chunked" . CRLF .
+                               "Connection: close" . CRLF .
+                               CRLF .
+                               "a" . CRLF .
+                               "finished" . CRLF .
+                               CRLF . "0" . CRLF . CRLF;
+               } elsif ($uri eq '/chunked/big') {
+                       print $client
+                               "HTTP/1.1 200 OK" . CRLF .
+                               "Transfer-Encoding: chunked" . CRLF .
+                               "Bar: xxx" . CRLF .
+                               "Connection: close" . CRLF .
+                               CRLF;
+
+                       for (1 .. 100) {
+                               print $client "ff" . CRLF . ("X" x 255) . CRLF;
+                       }
+
+                   print $client  "0" . CRLF . CRLF;
+               }
+       }
+}
+
+###############################################################################
diff --git a/nginx/t/js_fetch_https.t b/nginx/t/js_fetch_https.t
new file mode 100644 (file)
index 0000000..154bbbc
--- /dev/null
@@ -0,0 +1,283 @@
+#!/usr/bin/perl
+
+# (C) Antoine Bonavita
+# (C) Nginx, Inc.
+
+# Tests for http njs module, fetch method, https support.
+
+###############################################################################
+
+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 http_ssl 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;
+
+        resolver   127.0.0.1:%%PORT_8981_UDP%%;
+        resolver_timeout 1s;
+
+        location /njs {
+            js_content test.njs;
+        }
+
+        location /https {
+            js_content test.https;
+        }
+
+        location /https.myca {
+            js_content test.https;
+
+            js_fetch_ciphers HIGH:!aNull:!MD5;
+            js_fetch_protocols TLSv1.1 TLSv1.2;
+            js_fetch_trusted_certificate myca.crt;
+        }
+
+        location /https.myca.short {
+            js_content test.https;
+
+            js_fetch_verify_depth 0;
+            js_fetch_trusted_certificate myca.crt;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8081 ssl default;
+        server_name  default.example.com;
+
+        ssl_certificate default.example.com.chained.crt;
+        ssl_certificate_key default.example.com.key;
+
+        location /loc {
+            return 200 "You are at default.example.com.";
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8081 ssl;
+        server_name  1.example.com;
+
+        ssl_certificate 1.example.com.chained.crt;
+        ssl_certificate_key 1.example.com.key;
+
+        location /loc {
+            return 200 "You are at 1.example.com.";
+        }
+    }
+}
+
+EOF
+
+my $p1 = port(8081);
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function https(r) {
+        var url = `https://\${r.args.domain}:$p1/loc`;
+        var opt = {};
+
+        if (r.args.verify != null && r.args.verify == "false") {
+            opt.verify = false;
+        }
+
+        ngx.fetch(url, opt)
+        .then(reply => reply.text())
+        .then(body => r.return(200, body))
+        .catch(e => r.return(501, e.message))
+    }
+
+    export default {njs: test_njs, https};
+EOF
+
+my $d = $t->testdir();
+
+$t->write_file('openssl.conf', <<EOF);
+[ req ]
+default_bits = 2048
+encrypt_key = no
+distinguished_name = req_distinguished_name
+[ req_distinguished_name ]
+EOF
+
+$t->write_file('myca.conf', <<EOF);
+[ ca ]
+default_ca = myca
+
+[ myca ]
+new_certs_dir = $d
+database = $d/certindex
+default_md = sha256
+policy = myca_policy
+serial = $d/certserial
+default_days = 1
+x509_extensions = myca_extensions
+
+[ myca_policy ]
+commonName = supplied
+
+[ myca_extensions ]
+basicConstraints = critical,CA:TRUE
+EOF
+
+system('openssl req -x509 -new '
+       . "-config $d/openssl.conf -subj /CN=myca/ "
+       . "-out $d/myca.crt -keyout $d/myca.key "
+       . ">>$d/openssl.out 2>&1") == 0
+       or die "Can't create self-signed certificate for CA: $!\n";
+
+foreach my $name ('intermediate', 'default.example.com', '1.example.com') {
+       system("openssl req -new "
+               . "-config $d/openssl.conf -subj /CN=$name/ "
+               . "-out $d/$name.csr -keyout $d/$name.key "
+               . ">>$d/openssl.out 2>&1") == 0
+               or die "Can't create certificate signing req for $name: $!\n";
+}
+
+$t->write_file('certserial', '1000');
+$t->write_file('certindex', '');
+
+system("openssl ca -batch -config $d/myca.conf "
+       . "-keyfile $d/myca.key -cert $d/myca.crt "
+       . "-subj /CN=intermediate/ -in $d/intermediate.csr "
+       . "-out $d/intermediate.crt "
+       . ">>$d/openssl.out 2>&1") == 0
+       or die "Can't sign certificate for intermediate: $!\n";
+
+foreach my $name ('default.example.com', '1.example.com') {
+       system("openssl ca -batch -config $d/myca.conf "
+               . "-keyfile $d/intermediate.key -cert $d/intermediate.crt "
+               . "-subj /CN=$name/ -in $d/$name.csr -out $d/$name.crt "
+               . ">>$d/openssl.out 2>&1") == 0
+               or die "Can't sign certificate for $name $!\n";
+       $t->write_file("$name.chained.crt", $t->read_file("$name.crt")
+               . $t->read_file('intermediate.crt'));
+}
+
+$t->try_run('no njs.fetch')->plan(7);
+
+$t->run_daemon(\&dns_daemon, port(8981), $t);
+$t->waitforfile($t->testdir . '/' . port(8981));
+
+###############################################################################
+
+like(http_get('/https?domain=default.example.com&verify=false'),
+       qr/You are at default.example.com.$/s, 'fetch https');
+like(http_get('/https?domain=127.0.0.1&verify=false'),
+       qr/You are at default.example.com.$/s, 'fetch https by IP');
+like(http_get('/https?domain=1.example.com&verify=false'),
+       qr/You are at 1.example.com.$/s, 'fetch tls extension');
+like(http_get('/https.myca?domain=default.example.com'),
+       qr/You are at default.example.com.$/s, 'fetch https trusted certificate');
+like(http_get('/https.myca?domain=localhost'),
+       qr/connect failed/s, 'fetch https wrong CN certificate');
+like(http_get('/https?domain=default.example.com'),
+       qr/connect failed/s, 'fetch https non trusted CA');
+like(http_get('/https.myca.short?domain=default.example.com'),
+       qr/connect failed/s, 'fetch https CA too far');
+
+###############################################################################
+
+sub reply_handler {
+       my ($recv_data, $port, %extra) = @_;
+
+       my (@name, @rdata);
+
+       use constant NOERROR    => 0;
+       use constant A          => 1;
+       use constant IN         => 1;
+
+       # default values
+
+       my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 3600);
+
+       # decode name
+
+       my ($len, $offset) = (undef, 12);
+       while (1) {
+               $len = unpack("\@$offset C", $recv_data);
+               last if $len == 0;
+               $offset++;
+               push @name, unpack("\@$offset A$len", $recv_data);
+               $offset += $len;
+       }
+
+       $offset -= 1;
+       my ($id, $type, $class) = unpack("n x$offset n2", $recv_data);
+
+       my $name = join('.', @name);
+
+       if ($type == A) {
+               push @rdata, rd_addr($ttl, '127.0.0.1');
+       }
+
+       $len = @name;
+       pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata,
+               0, 0, @name, $type, $class) . join('', @rdata);
+}
+
+sub rd_addr {
+       my ($ttl, $addr) = @_;
+
+       my $code = 'split(/\./, $addr)';
+
+       return pack 'n3N', 0xc00c, A, IN, $ttl if $addr eq '';
+
+       pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code);
+}
+
+sub dns_daemon {
+       my ($port, $t) = @_;
+
+       my ($data, $recv_data);
+       my $socket = IO::Socket::INET->new(
+               LocalAddr    => '127.0.0.1',
+               LocalPort    => $port,
+               Proto        => 'udp',
+       )
+               or die "Can't create listening socket: $!\n";
+
+       local $SIG{PIPE} = 'IGNORE';
+
+       # signal we are ready
+
+       open my $fh, '>', $t->testdir() . '/' . $port;
+       close $fh;
+
+       while (1) {
+               $socket->recv($recv_data, 65536);
+               $data = reply_handler($recv_data, $port);
+               $socket->send($data);
+       }
+}
+
+###############################################################################
diff --git a/nginx/t/js_fetch_objects.t b/nginx/t/js_fetch_objects.t
new file mode 100644 (file)
index 0000000..9f23599
--- /dev/null
@@ -0,0 +1,500 @@
+#!/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'],
+            ['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'],
+            ['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;
+}
+
+###############################################################################
diff --git a/nginx/t/js_fetch_resolver.t b/nginx/t/js_fetch_resolver.t
new file mode 100644 (file)
index 0000000..8b0dc45
--- /dev/null
@@ -0,0 +1,231 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, fetch method, dns support.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+use IO::Select;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+plan(skip_all => '127.0.0.2 local address required')
+       unless defined IO::Socket::INET->new( LocalAddr => '127.0.0.2' );
+
+my $t = Test::Nginx->new()->has(qw/http/)
+       ->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 /dns {
+            js_content test.dns;
+
+            resolver   127.0.0.1:%%PORT_8981_UDP%%;
+            resolver_timeout 1s;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  aaa;
+
+        location /loc {
+            js_content test.loc;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  many;
+
+        location /loc {
+            js_content test.loc;
+        }
+    }
+}
+
+EOF
+
+my $p0 = port(8080);
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function dns(r) {
+        var url = `http://\${r.args.domain}:$p0/loc`;
+
+        ngx.fetch(url)
+        .then(reply => reply.text())
+        .then(body => r.return(200, body))
+        .catch(e => r.return(501, e.message))
+    }
+
+    function str(v) { return v ? v : ''};
+
+    function loc(r) {
+        var v = r.variables;
+        var body = str(r.requestText);
+        var foo = str(r.headersIn.foo);
+        var bar = str(r.headersIn.bar);
+        var c = r.headersIn.code ? Number(r.headersIn.code) : 200;
+        r.return(c, `\${v.host}:\${v.request_method}:\${foo}:\${bar}:\${body}`);
+    }
+
+     export default {njs: test_njs, dns, loc};
+EOF
+
+$t->try_run('no njs.fetch')->plan(3);
+
+$t->run_daemon(\&dns_daemon, port(8981), $t);
+$t->waitforfile($t->testdir . '/' . port(8981));
+
+###############################################################################
+
+like(http_get('/dns?domain=aaa'), qr/aaa:GET:::$/s, 'fetch dns aaa');
+like(http_get('/dns?domain=many'), qr/many:GET:::$/s, 'fetch dns many');
+like(http_get('/dns?domain=unknown'), qr/"unknown" could not be resolved/s,
+       'fetch dns unknown');
+
+###############################################################################
+
+sub reply_handler {
+       my ($recv_data, $port, %extra) = @_;
+
+       my (@name, @rdata);
+
+       use constant NOERROR    => 0;
+       use constant FORMERR    => 1;
+       use constant SERVFAIL   => 2;
+       use constant NXDOMAIN   => 3;
+
+       use constant A          => 1;
+
+       use constant IN         => 1;
+
+       # default values
+
+       my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 3600);
+
+       # decode name
+
+       my ($len, $offset) = (undef, 12);
+       while (1) {
+               $len = unpack("\@$offset C", $recv_data);
+               last if $len == 0;
+               $offset++;
+               push @name, unpack("\@$offset A$len", $recv_data);
+               $offset += $len;
+       }
+
+       $offset -= 1;
+       my ($id, $type, $class) = unpack("n x$offset n2", $recv_data);
+
+       my $name = join('.', @name);
+
+       if ($name eq 'aaa' && $type == A) {
+               push @rdata, rd_addr($ttl, '127.0.0.1');
+
+       } elsif ($name eq 'many' && $type == A) {
+               push @rdata, rd_addr($ttl, '127.0.0.2');
+               push @rdata, rd_addr($ttl, '127.0.0.1');
+       }
+
+       $len = @name;
+       pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata,
+               0, 0, @name, $type, $class) . join('', @rdata);
+}
+
+sub rd_addr {
+       my ($ttl, $addr) = @_;
+
+       my $code = 'split(/\./, $addr)';
+
+       return pack 'n3N', 0xc00c, A, IN, $ttl if $addr eq '';
+
+       pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code);
+}
+
+sub dns_daemon {
+       my ($port, $t, %extra) = @_;
+
+       my ($data, $recv_data);
+       my $socket = IO::Socket::INET->new(
+               LocalAddr => '127.0.0.1',
+               LocalPort => $port,
+               Proto => 'udp',
+       )
+               or die "Can't create listening socket: $!\n";
+
+       my $sel = IO::Select->new($socket);
+
+       local $SIG{PIPE} = 'IGNORE';
+
+       # signal we are ready
+
+       open my $fh, '>', $t->testdir() . '/' . $port;
+       close $fh;
+
+       while (my @ready = $sel->can_read) {
+               foreach my $fh (@ready) {
+                       if ($socket == $fh) {
+                               $fh->recv($recv_data, 65536);
+                               $data = reply_handler($recv_data, $port);
+                               $fh->send($data);
+
+                       } else {
+                               $fh->recv($recv_data, 65536);
+                               unless (length $recv_data) {
+                                       $sel->remove($fh);
+                                       $fh->close;
+                                       next;
+                               }
+
+again:
+                               my $len = unpack("n", $recv_data);
+                               $data = substr $recv_data, 2, $len;
+                               $data = reply_handler($data, $port, tcp => 1);
+                               $data = pack("n", length $data) . $data;
+                               $fh->send($data);
+                               $recv_data = substr $recv_data, 2 + $len;
+                               goto again if length $recv_data;
+                       }
+               }
+       }
+}
+
+###############################################################################
diff --git a/nginx/t/js_fetch_timeout.t b/nginx/t/js_fetch_timeout.t
new file mode 100644 (file)
index 0000000..486656d
--- /dev/null
@@ -0,0 +1,119 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, fetch method timeout.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+use Socket qw/ CRLF /;
+
+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/)
+       ->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 /normal_timeout {
+            js_content test.timeout_test;
+        }
+
+        location /short_timeout {
+            js_fetch_timeout 200ms;
+            js_content test.timeout_test;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8081;
+        server_name  localhost;
+
+        location /normal_reply {
+            js_content test.normal_reply;
+        }
+
+        location /delayed_reply {
+            js_content test.delayed_reply;
+        }
+    }
+}
+
+EOF
+
+my $p1 = port(8081);
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    async function timeout_test(r) {
+        let rs = await Promise.allSettled([
+            'http://127.0.0.1:$p1/normal_reply',
+            'http://127.0.0.1:$p1/delayed_reply',
+        ].map(v => ngx.fetch(v)));
+
+        let bs = rs.map(v => ({s: v.status, v: v.value ? v.value.headers.X
+                                                       : v.reason}));
+
+        r.return(200, njs.dump(bs));
+    }
+
+    function normal_reply(r) {
+        r.headersOut.X = 'N';
+        r.return(200);
+    }
+
+    function delayed_reply(r) {
+        r.headersOut.X = 'D';
+        setTimeout((r) => { r.return(200); }, 250, r, 0);
+    }
+
+     export default {njs: test_njs, timeout_test, normal_reply, delayed_reply};
+EOF
+
+$t->try_run('no js_fetch_timeout')->plan(2);
+
+###############################################################################
+
+like(http_get('/normal_timeout'),
+       qr/\[\{s:'fulfilled',v:'N'},\{s:'fulfilled',v:'D'}]$/s,
+       'normal timeout');
+like(http_get('/short_timeout'),
+       qr/\[\{s:'fulfilled',v:'N'},\{s:'rejected',v:Error: read timed out}]$/s,
+       'short timeout');
+
+###############################################################################
diff --git a/nginx/t/js_fetch_verify.t b/nginx/t/js_fetch_verify.t
new file mode 100644 (file)
index 0000000..d6bb1d9
--- /dev/null
@@ -0,0 +1,192 @@
+#!/usr/bin/perl
+
+# (C) Sergey Kandaurov
+# (C) Nginx, Inc.
+
+# Tests for http njs module, fetch method, backend certificate verification.
+
+###############################################################################
+
+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 http_ssl/)
+       ->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;
+
+        resolver   127.0.0.1:%%PORT_8981_UDP%%;
+        resolver_timeout 1s;
+
+        location /njs {
+            js_content test.njs;
+        }
+
+        location /https {
+            js_content test.https;
+        }
+
+        location /https.verify_off {
+            js_content test.https;
+            js_fetch_verify off;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8081 ssl;
+        server_name  localhost;
+
+        ssl_certificate localhost.crt;
+        ssl_certificate_key localhost.key;
+    }
+}
+
+EOF
+
+my $p1 = port(8081);
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function https(r) {
+        ngx.fetch(`https://example.com:$p1/loc`)
+        .then(reply => reply.text())
+        .then(body => r.return(200, body))
+        .catch(e => r.return(501, e.message));
+    }
+
+    export default {njs: test_njs, https};
+EOF
+
+$t->write_file('openssl.conf', <<EOF);
+[ req ]
+default_bits = 2048
+encrypt_key = no
+distinguished_name = req_distinguished_name
+[ req_distinguished_name ]
+EOF
+
+my $d = $t->testdir();
+
+foreach my $name ('localhost') {
+       system('openssl req -x509 -new '
+               . "-config $d/openssl.conf -subj /CN=$name/ "
+               . "-out $d/$name.crt -keyout $d/$name.key "
+               . ">>$d/openssl.out 2>&1") == 0
+               or die "Can't create certificate for $name: $!\n";
+}
+
+$t->try_run('no js_fetch_verify')->plan(2);
+
+$t->run_daemon(\&dns_daemon, port(8981), $t);
+$t->waitforfile($t->testdir . '/' . port(8981));
+
+###############################################################################
+
+like(http_get('/https'), qr/connect failed/, 'fetch verify error');
+like(http_get('/https.verify_off'), qr/200 OK/, 'fetch verify off');
+
+###############################################################################
+
+sub reply_handler {
+       my ($recv_data, $port, %extra) = @_;
+
+       my (@name, @rdata);
+
+       use constant NOERROR    => 0;
+       use constant A          => 1;
+       use constant IN         => 1;
+
+       # default values
+
+       my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 3600);
+
+       # decode name
+
+       my ($len, $offset) = (undef, 12);
+       while (1) {
+               $len = unpack("\@$offset C", $recv_data);
+               last if $len == 0;
+               $offset++;
+               push @name, unpack("\@$offset A$len", $recv_data);
+               $offset += $len;
+       }
+
+       $offset -= 1;
+       my ($id, $type, $class) = unpack("n x$offset n2", $recv_data);
+
+       my $name = join('.', @name);
+
+       if ($type == A) {
+               push @rdata, rd_addr($ttl, '127.0.0.1');
+       }
+
+       $len = @name;
+       pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata,
+               0, 0, @name, $type, $class) . join('', @rdata);
+}
+
+sub rd_addr {
+       my ($ttl, $addr) = @_;
+
+       my $code = 'split(/\./, $addr)';
+
+       return pack 'n3N', 0xc00c, A, IN, $ttl if $addr eq '';
+
+       pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code);
+}
+
+sub dns_daemon {
+       my ($port, $t) = @_;
+
+       my ($data, $recv_data);
+       my $socket = IO::Socket::INET->new(
+               LocalAddr    => '127.0.0.1',
+               LocalPort    => $port,
+               Proto        => 'udp',
+       )
+               or die "Can't create listening socket: $!\n";
+
+       local $SIG{PIPE} = 'IGNORE';
+
+       # signal we are ready
+
+       open my $fh, '>', $t->testdir() . '/' . $port;
+       close $fh;
+
+       while (1) {
+               $socket->recv($recv_data, 65536);
+               $data = reply_handler($recv_data, $port);
+               $socket->send($data);
+       }
+}
+
+###############################################################################
diff --git a/nginx/t/js_header_filter.t b/nginx/t/js_header_filter.t
new file mode 100644 (file)
index 0000000..aecac34
--- /dev/null
@@ -0,0 +1,93 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, header filter.
+
+###############################################################################
+
+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 proxy 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 /filter/ {
+            js_header_filter test.filter;
+            proxy_pass http://127.0.0.1:8081/;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8081;
+        server_name  localhost;
+
+        location / {
+            add_header Set-Cookie "BB";
+            add_header Set-Cookie "CCCC";
+
+            return 200;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function filter(r) {
+        var cookies = r.headersOut['Set-Cookie'];
+        var len = r.args.len ? Number(r.args.len) : 0;
+        r.headersOut['Set-Cookie'] = cookies.filter(v=>v.length > len);
+    }
+
+    export default {njs: test_njs, filter};
+
+EOF
+
+$t->try_run('no njs header filter')->plan(2);
+
+###############################################################################
+
+like(http_get('/filter/?len=1'), qr/Set-Cookie: BB.*Set-Cookie: CCCC.*/ms,
+       'all');;
+unlike(http_get('/filter/?len=3'), qr/Set-Cookie: BB/,
+       'filter');
+
+###############################################################################
diff --git a/nginx/t/js_header_filter_if.t b/nginx/t/js_header_filter_if.t
new file mode 100644 (file)
index 0000000..a6ab3c4
--- /dev/null
@@ -0,0 +1,95 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, header filter, if context.
+
+###############################################################################
+
+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 proxy 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 / {
+            if ($arg_name ~ "add") {
+                js_header_filter test.add;
+            }
+
+            js_header_filter test.add2;
+
+            proxy_pass http://127.0.0.1:8081/;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8081;
+        server_name  localhost;
+
+        location / {
+            return 200;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function add(r) {
+        r.headersOut['Foo'] = 'bar';
+    }
+
+    function add2(r) {
+        r.headersOut['Bar'] = 'xxx';
+    }
+
+    export default {njs: test_njs, add, add2};
+
+EOF
+
+$t->try_run('no njs header filter')->plan(2);
+
+###############################################################################
+
+like(http_get('/?name=add'), qr/Foo: bar/, 'header filter if');
+like(http_get('/'), qr/Bar: xxx/, 'header filter');
+
+###############################################################################
diff --git a/nginx/t/js_headers.t b/nginx/t/js_headers.t
new file mode 100644 (file)
index 0000000..6f08dcf
--- /dev/null
@@ -0,0 +1,568 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, working with headers.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+use Socket qw/ CRLF /;
+
+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 charset/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    js_set $test_foo_in   test.foo_in;
+    js_set $test_ifoo_in  test.ifoo_in;
+
+    js_import test.js;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /njs {
+            js_content test.njs;
+        }
+
+        location /content_length {
+            js_content test.content_length;
+        }
+
+        location /content_length_arr {
+            js_content test.content_length_arr;
+        }
+
+        location /content_length_keys {
+            js_content test.content_length_keys;
+        }
+
+        location /content_type {
+            charset windows-1251;
+
+            default_type text/plain;
+            js_content test.content_type;
+        }
+
+        location /content_type_arr {
+            charset windows-1251;
+
+            default_type text/plain;
+            js_content test.content_type_arr;
+        }
+
+        location /content_encoding {
+            js_content test.content_encoding;
+        }
+
+        location /content_encoding_arr {
+            js_content test.content_encoding_arr;
+        }
+
+        location /headers_list {
+            js_content test.headers_list;
+        }
+
+        location /foo_in {
+            return 200 $test_foo_in;
+        }
+
+        location /ifoo_in {
+            return 200 $test_ifoo_in;
+        }
+
+        location /hdr_in {
+            js_content test.hdr_in;
+        }
+
+        location /raw_hdr_in {
+            js_content test.raw_hdr_in;
+        }
+
+        location /hdr_out {
+            js_content test.hdr_out;
+        }
+
+        location /raw_hdr_out {
+            js_content test.raw_hdr_out;
+        }
+
+        location /hdr_out_array {
+            js_content test.hdr_out_array;
+        }
+
+        location /hdr_out_set_cookie {
+            js_content test.hdr_out_set_cookie;
+        }
+
+        location /hdr_out_single {
+            js_content test.hdr_out_single;
+        }
+
+        location /ihdr_out {
+            js_content test.ihdr_out;
+        }
+
+        location /hdr_sorted_keys {
+            js_content test.hdr_sorted_keys;
+        }
+
+        location /hdr_out_special_set {
+            js_content test.hdr_out_special_set;
+        }
+
+        location /copy_subrequest_hdrs {
+            js_content test.copy_subrequest_hdrs;
+        }
+
+        location = /subrequest {
+            internal;
+
+            js_content test.subrequest;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function content_length(r) {
+        if (njs.version_number >= 0x000705) {
+            var clength = r.headersOut['Content-Length'];
+            if (clength !== undefined) {
+                r.return(500, `Content-Length "\${clength}" is not empty`);
+                return;
+            }
+        }
+
+        delete r.headersOut['Content-Length'];
+        r.headersOut['Content-Length'] = '';
+        r.headersOut['Content-Length'] = 3;
+        delete r.headersOut['Content-Length'];
+        r.headersOut['Content-Length'] = 3;
+        r.sendHeader();
+        r.send('XXX');
+        r.finish();
+    }
+
+    function content_length_arr(r) {
+        r.headersOut['Content-Length'] = [5];
+        r.headersOut['Content-Length'] = [];
+        r.headersOut['Content-Length'] = [4,3];
+        r.sendHeader();
+        r.send('XXX');
+        r.finish();
+    }
+
+    function content_length_keys(r) {
+        r.headersOut['Content-Length'] = 3;
+        var in_keys = Object.keys(r.headersOut).some(v=>v=='Content-Length');
+        r.return(200, `B:\${in_keys}`);
+    }
+
+    function content_type(r) {
+        if (njs.version_number >= 0x000705) {
+            var ctype = r.headersOut['Content-Type'];
+            if (ctype !== undefined) {
+                r.return(500, `Content-Type "\${ctype}" is not empty`);
+                return;
+            }
+        }
+
+        delete r.headersOut['Content-Type'];
+        r.headersOut['Content-Type'] = 'text/xml';
+        r.headersOut['Content-Type'] = '';
+        r.headersOut['Content-Type'] = 'text/xml; charset=';
+        delete r.headersOut['Content-Type'];
+        r.headersOut['Content-Type'] = 'text/xml; charset=utf-8';
+        r.headersOut['Content-Type'] = 'text/xml; charset="utf-8"';
+        var in_keys = Object.keys(r.headersOut).some(v=>v=='Content-Type');
+        r.return(200, `B:\${in_keys}`);
+    }
+
+    function content_type_arr(r) {
+        r.headersOut['Content-Type'] = ['text/html'];
+        r.headersOut['Content-Type'] = [];
+        r.headersOut['Content-Type'] = [ 'text/xml', 'text/html'];
+        r.return(200);
+    }
+
+    function content_encoding(r) {
+        if (njs.version_number >= 0x000705) {
+            var ce = r.headersOut['Content-Encoding'];
+            if (ce !== undefined) {
+                r.return(500, `Content-Encoding "\${ce}" is not empty`);
+                return;
+            }
+        }
+
+        delete r.headersOut['Content-Encoding'];
+        r.headersOut['Content-Encoding'] = '';
+        r.headersOut['Content-Encoding'] = 'test';
+        delete r.headersOut['Content-Encoding'];
+        r.headersOut['Content-Encoding'] = 'gzip';
+        r.return(200);
+    }
+
+    function content_encoding_arr(r) {
+        r.headersOut['Content-Encoding'] = 'test';
+        r.headersOut['Content-Encoding'] = [];
+        r.headersOut['Content-Encoding'] = ['test', 'gzip'];
+        r.return(200);
+    }
+
+    function headers_list(r) {
+        for (var h in {a:1, b:2, c:3}) {
+            r.headersOut[h] = h;
+        }
+
+        delete r.headersOut.b;
+        r.headersOut.d = 'd';
+
+        var out = "";
+        for (var h in r.headersOut) {
+            out += h + ":";
+        }
+
+        r.return(200, out);
+    }
+
+    function hdr_in(r) {
+        var s = '', h;
+        for (h in r.headersIn) {
+            s += `\${h.toLowerCase()}: \${r.headersIn[h]}\n`;
+        }
+
+        r.return(200, s);
+    }
+
+    function raw_hdr_in(r) {
+        var filtered = r.rawHeadersIn
+                       .filter(v=>v[0].toLowerCase() == r.args.filter);
+        r.return(200, 'raw:' + filtered.map(v=>v[1]).join('|'));
+    }
+
+    function hdr_sorted_keys(r) {
+        var s = '';
+        var hdr = r.args.in ? r.headersIn : r.headersOut;
+
+        if (!r.args.in) {
+            r.headersOut.b = 'b';
+            r.headersOut.c = 'c';
+            r.headersOut.a = 'a';
+        }
+
+        r.return(200, Object.keys(hdr).sort());
+    }
+
+    function foo_in(r) {
+        return 'hdr=' + r.headersIn.foo;
+    }
+
+    function ifoo_in(r) {
+        var s = '', h;
+        for (h in r.headersIn) {
+            if (h.substr(0, 3) == 'foo') {
+                s += r.headersIn[h];
+            }
+        }
+        return s;
+    }
+
+    function hdr_out(r) {
+        r.status = 200;
+        r.headersOut['Foo'] = r.args.foo;
+
+        if (r.args.bar) {
+            r.headersOut['Bar'] =
+                r.headersOut[(r.args.bar == 'empty' ? 'Baz' :'Foo')]
+        }
+
+        r.sendHeader();
+        r.finish();
+    }
+
+    function raw_hdr_out(r) {
+        r.headersOut.a = ['foo', 'bar'];
+        r.headersOut.b = 'b';
+
+        var filtered = r.rawHeadersOut
+                       .filter(v=>v[0].toLowerCase() == r.args.filter);
+        r.return(200, 'raw:' + filtered.map(v=>v[1]).join('|'));
+    }
+
+    function hdr_out_array(r) {
+        if (!r.args.hidden) {
+            r.headersOut['Foo'] = [r.args.foo];
+            r.headersOut['Foo'] = [];
+            r.headersOut['Foo'] = ['bar', r.args.foo];
+        }
+
+        if (r.args.scalar_set) {
+            r.headersOut['Foo'] = 'xxx';
+        }
+
+        r.return(200, `B:\${njs.dump(r.headersOut.foo)}`);
+    }
+
+    function hdr_out_single(r) {
+        r.headersOut.ETag = ['a', 'b'];
+        r.return(200, `B:\${njs.dump(r.headersOut.etag)}`);
+    }
+
+    function hdr_out_set_cookie(r) {
+        r.headersOut['Set-Cookie'] = [];
+        r.headersOut['Set-Cookie'] = ['a', 'b'];
+        delete r.headersOut['Set-Cookie'];
+        r.headersOut['Set-Cookie'] = 'e';
+        r.headersOut['Set-Cookie'] = ['c', '', null, 'd', 'f'];
+
+        r.return(200, `B:\${njs.dump(r.headersOut['Set-Cookie'])}`);
+    }
+
+    function ihdr_out(r) {
+        r.status = 200;
+        r.headersOut['a'] = r.args.a;
+        r.headersOut['b'] = r.args.b;
+
+        var s = '', h;
+        for (h in r.headersOut) {
+            s += r.headersOut[h];
+        }
+
+        r.sendHeader();
+        r.send(s);
+        r.finish();
+    }
+
+    function hdr_out_special_set(r) {
+        r.headersOut['Foo'] = "xxx";
+        r.headersOut['Content-Encoding'] = 'abc';
+
+        let ce = r.headersOut['Content-Encoding'];
+        r.return(200, `CE: \${ce}`);
+    }
+
+    async function copy_subrequest_hdrs(r) {
+        let resp = await r.subrequest("/subrequest");
+
+        for (const h in resp.headersOut) {
+            r.headersOut[h] = resp.headersOut[h];
+        }
+
+        r.return(200, resp.responseText);
+    }
+
+    function subrequest(r) {
+        r.headersOut['A'] = 'a';
+        r.headersOut['Content-Encoding'] = 'ce';
+        r.headersOut['B'] = 'b';
+        r.headersOut['C'] = 'c';
+        r.headersOut['D'] = 'd';
+        r.headersOut['Set-Cookie'] = ['A', 'BB'];
+        r.headersOut['Content-Length'] = 3;
+        r.headersOut['Content-Type'] = 'ct';
+        r.sendHeader();
+        r.send('XXX');
+        r.finish();
+    }
+
+    export default {njs:test_njs, content_length, content_length_arr,
+                    content_length_keys, content_type, content_type_arr,
+                    content_encoding, content_encoding_arr, headers_list,
+                    hdr_in, raw_hdr_in, hdr_sorted_keys, foo_in, ifoo_in,
+                    hdr_out, raw_hdr_out, hdr_out_array, hdr_out_single,
+                    hdr_out_set_cookie, ihdr_out, hdr_out_special_set,
+                    copy_subrequest_hdrs, subrequest};
+
+
+EOF
+
+$t->try_run('no njs')->plan(42);
+
+###############################################################################
+
+like(http_get('/content_length'), qr/Content-Length: 3/,
+       'set Content-Length');
+like(http_get('/content_type'), qr/Content-Type: text\/xml; charset="utf-8"\r/,
+       'set Content-Type');
+unlike(http_get('/content_type'), qr/Content-Type: text\/plain/,
+       'set Content-Type 2');
+like(http_get('/content_encoding'), qr/Content-Encoding: gzip/,
+       'set Content-Encoding');
+like(http_get('/headers_list'), qr/a:c:d/, 'headers list');
+
+like(http_get('/ihdr_out?a=12&b=34'), qr/^1234$/m, 'r.headersOut iteration');
+like(http_get('/ihdr_out'), qr/\x0d\x0a?\x0d\x0a?$/m, 'r.send zero');
+like(http_get('/hdr_out?foo=12345'), qr/Foo: 12345/, 'r.headersOut');
+like(http_get('/hdr_out?foo=123&bar=copy'), qr/Bar: 123/, 'r.headersOut get');
+unlike(http_get('/hdr_out?bar=empty'), qr/Bar:/, 'r.headersOut empty');
+unlike(http_get('/hdr_out?foo='), qr/Foo:/, 'r.headersOut no value');
+unlike(http_get('/hdr_out?foo'), qr/Foo:/, 'r.headersOut no value 2');
+
+like(http_get('/content_length_keys'), qr/B:true/, 'Content-Length in keys');
+like(http_get('/content_length_arr'), qr/Content-Length: 3/,
+       'set Content-Length arr');
+
+like(http_get('/content_type'), qr/B:true/, 'Content-Type in keys');
+like(http_get('/content_type_arr'), qr/Content-Type: text\/html/,
+       'set Content-Type arr');
+like(http_get('/content_encoding_arr'), qr/Content-Encoding: gzip/,
+       'set Content-Encoding arr');
+
+like(http_get('/hdr_out_array?foo=12345'), qr/Foo: bar\r\nFoo: 12345/,
+       'r.headersOut arr');
+like(http_get('/hdr_out_array'), qr/Foo: bar/,
+       'r.headersOut arr last is empty');
+like(http_get('/hdr_out_array?foo=abc'), qr/B:bar,\s?abc/,
+       'r.headersOut get');
+like(http_get('/hdr_out_array'), qr/B:bar/, 'r.headersOut get2');
+like(http_get('/hdr_out_array?hidden=1'), qr/B:undefined/,
+       'r.headersOut get3');
+like(http_get('/hdr_out_array?scalar_set=1'), qr/B:xxx/,
+       'r.headersOut scalar set');
+like(http_get('/hdr_out_single'), qr/ETag: a\r\nETag: b/,
+       'r.headersOut single');
+like(http_get('/hdr_out_single'), qr/B:a/,
+       'r.headersOut single get');
+like(http_get('/hdr_out_set_cookie'), qr/Set-Cookie: c\r\nSet-Cookie: d/,
+       'set_cookie');
+like(http_get('/hdr_out_set_cookie'), qr/B:\['c','d','f']/,
+       'set_cookie2');
+unlike(http_get('/hdr_out_set_cookie'), qr/Set-Cookie: [abe]/,
+       'set_cookie3');
+
+like(http(
+       'GET /hdr_in HTTP/1.0' . CRLF
+       . 'Cookie: foo' . CRLF
+       . 'Host: localhost' . CRLF . CRLF
+), qr/cookie: foo/, 'r.headersIn cookie');
+
+like(http(
+       'GET /hdr_in HTTP/1.0' . CRLF
+       . 'X-Forwarded-For: foo' . CRLF
+       . 'Host: localhost' . CRLF . CRLF
+), qr/x-forwarded-for: foo/, 'r.headersIn xff');
+
+like(http(
+       'GET /hdr_in HTTP/1.0' . CRLF
+       . 'Cookie: foo1' . CRLF
+       . 'Cookie: foo2' . CRLF
+       . 'Host: localhost' . CRLF . CRLF
+), qr/cookie: foo1;\s?foo2/, 'r.headersIn cookie2');
+
+like(http(
+       'GET /hdr_in HTTP/1.0' . CRLF
+       . 'X-Forwarded-For: foo1' . CRLF
+       . 'X-Forwarded-For: foo2' . CRLF
+       . 'Host: localhost' . CRLF . CRLF
+), qr/x-forwarded-for: foo1,\s?foo2/, 'r.headersIn xff2');
+
+like(http(
+       'GET /hdr_in HTTP/1.0' . CRLF
+       . 'ETag: bar1' . CRLF
+       . 'ETag: bar2' . CRLF
+       . 'Host: localhost' . CRLF . CRLF
+), qr/etag: bar1(?!,\s?bar2)/, 'r.headersIn duplicate single');
+
+like(http(
+       'GET /hdr_in HTTP/1.0' . CRLF
+       . 'Content-Type: bar1' . CRLF
+       . 'Content-Type: bar2' . CRLF
+       . 'Host: localhost' . CRLF . CRLF
+), qr/content-type: bar1(?!,\s?bar2)/, 'r.headersIn duplicate single 2');
+
+like(http(
+       'GET /hdr_in HTTP/1.0' . CRLF
+       . 'Foo: bar1' . CRLF
+       . 'Foo: bar2' . CRLF
+       . 'Host: localhost' . CRLF . CRLF
+), qr/foo: bar1,\s?bar2/, 'r.headersIn duplicate generic');
+
+like(http(
+       'GET /raw_hdr_in?filter=foo HTTP/1.0' . CRLF
+       . 'foo: bar1' . CRLF
+       . 'Foo: bar2' . CRLF
+       . 'Host: localhost' . CRLF . CRLF
+), qr/raw: bar1|bar2/, 'r.rawHeadersIn');
+
+like(http_get('/raw_hdr_out?filter=a'), qr/raw: foo|bar/, 'r.rawHeadersOut');
+
+like(http(
+       'GET /hdr_sorted_keys?in=1 HTTP/1.0' . CRLF
+       . 'Cookie: foo1' . CRLF
+       . 'Accept: */*' . CRLF
+       . 'Cookie: foo2' . CRLF
+       . 'Host: localhost' . CRLF . CRLF
+), qr/Accept,Cookie,Host/, 'r.headersIn sorted keys');
+
+like(http(
+       'GET /hdr_sorted_keys HTTP/1.0' . CRLF
+       . 'Host: localhost' . CRLF . CRLF
+), qr/a,b,c/, 'r.headersOut sorted keys');
+
+TODO: {
+local $TODO = 'not yet' unless has_version('0.7.6');
+
+like(http_get('/hdr_out_special_set'), qr/CE: abc/,
+       'r.headerOut special set');
+
+like(http_get('/copy_subrequest_hdrs'),
+       qr/A: a.*B: b.*C: c.*D: d.*Set-Cookie: A.*Set-Cookie: BB/s,
+       'subrequest copy');
+
+like(http_get('/copy_subrequest_hdrs'),
+       qr/Content-Type: ct.*Content-Encoding: ce.*Content-Length: 3/s,
+       'subrequest copy special');
+
+}
+
+###############################################################################
+
+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;
+}
+
+###############################################################################
diff --git a/nginx/t/js_import.t b/nginx/t/js_import.t
new file mode 100644 (file)
index 0000000..52ee795
--- /dev/null
@@ -0,0 +1,108 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (c) Nginx, Inc.
+
+# Tests for http njs module, js_import directive.
+
+###############################################################################
+
+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/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    js_set $test foo.bar.p;
+
+    js_import lib.js;
+    js_import fun.js;
+    js_import foo from ./main.js;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /njs {
+            js_content foo.version;
+        }
+
+        location /test_foo {
+            js_content foo.test;
+        }
+
+        location /test_lib {
+            js_content lib.test;
+        }
+
+        location /test_fun {
+            js_content fun;
+        }
+
+        location /test_var {
+            return 200 $test;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('lib.js', <<EOF);
+    function test(r) {
+        r.return(200, "LIB-TEST");
+    }
+
+    export default {test};
+
+EOF
+
+$t->write_file('fun.js', <<EOF);
+    export default function (r) {r.return(200, "FUN-TEST")};
+
+EOF
+
+$t->write_file('main.js', <<EOF);
+    function version(r) {
+        r.return(200, njs.version);
+    }
+
+    function test(r) {
+        r.return(200, "MAIN-TEST");
+    }
+
+    export default {version, test, bar: {p(r) {return "P-TEST"}}};
+
+EOF
+
+$t->try_run('no njs available')->plan(4);
+
+###############################################################################
+
+like(http_get('/test_foo'), qr/MAIN-TEST/s, 'foo.test');
+like(http_get('/test_lib'), qr/LIB-TEST/s, 'lib.test');
+like(http_get('/test_fun'), qr/FUN-TEST/s, 'fun');
+like(http_get('/test_var'), qr/P-TEST/s, 'foo.bar.p');
+
+###############################################################################
diff --git a/nginx/t/js_import2.t b/nginx/t/js_import2.t
new file mode 100644 (file)
index 0000000..cd29d2d
--- /dev/null
@@ -0,0 +1,127 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (c) Nginx, Inc.
+
+# Tests for http njs module, js_import directive in server | location contexts.
+
+###############################################################################
+
+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 proxy rewrite/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        js_set $test foo.bar.p;
+
+        js_import foo from main.js;
+
+        location /njs {
+            js_content foo.version;
+        }
+
+        location /test_foo {
+            js_content foo.test;
+        }
+
+        location /test_lib {
+            js_import lib.js;
+            js_content lib.test;
+        }
+
+        location /test_fun {
+            js_import fun.js;
+            js_content fun;
+        }
+
+        location /test_var {
+            return 200 $test;
+        }
+
+        location /proxy {
+            proxy_pass http://127.0.0.1:8081/;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8081;
+        server_name  localhost;
+
+        location /test_fun {
+            js_import fun.js;
+            js_content fun;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('lib.js', <<EOF);
+    function test(r) {
+        r.return(200, "LIB-TEST");
+    }
+
+    function p(r) {
+        return "LIB-P";
+    }
+
+    export default {test, p};
+
+EOF
+
+$t->write_file('fun.js', <<EOF);
+    export default function (r) {r.return(200, "FUN-TEST")};
+
+EOF
+
+$t->write_file('main.js', <<EOF);
+    function version(r) {
+        r.return(200, njs.version);
+    }
+
+    function test(r) {
+        r.return(200, "MAIN-TEST");
+    }
+
+    export default {version, test, bar: {p(r) {return "P-TEST"}}};
+
+EOF
+
+$t->try_run('no njs available')->plan(5);
+
+###############################################################################
+
+like(http_get('/test_foo'), qr/MAIN-TEST/s, 'foo.test');
+like(http_get('/test_lib'), qr/LIB-TEST/s, 'lib.test');
+like(http_get('/test_fun'), qr/FUN-TEST/s, 'fun');
+like(http_get('/proxy/test_fun'), qr/FUN-TEST/s, 'proxy fun');
+like(http_get('/test_var'), qr/P-TEST/s, 'foo.bar.p');
+
+###############################################################################
diff --git a/nginx/t/js_internal_redirect.t b/nginx/t/js_internal_redirect.t
new file mode 100644 (file)
index 0000000..ec6be4e
--- /dev/null
@@ -0,0 +1,107 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, internalRedirect method.
+
+###############################################################################
+
+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 /test {
+            js_content test.redirect;
+        }
+
+        location /redirect {
+            internal;
+            return 200 redirect$arg_b;
+        }
+
+        location @named {
+            return 200 named;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function redirect(r) {
+        if (r.variables.arg_dest == 'named') {
+            r.internalRedirect('\@named');
+
+        } else if (r.variables.arg_unsafe) {
+            r.internalRedirect('/red\0rect');
+
+        } else if (r.variables.arg_quoted) {
+            r.internalRedirect('/red%69rect');
+
+        } else {
+            if (r.variables.arg_a) {
+                r.internalRedirect('/redirect?b=' + r.variables.arg_a);
+
+            } else {
+                r.internalRedirect('/redirect');
+            }
+        }
+    }
+
+    export default {njs:test_njs, redirect};
+
+EOF
+
+$t->try_run('no njs available')->plan(5);
+
+###############################################################################
+
+like(http_get('/test'), qr/redirect/s, 'redirect');
+like(http_get('/test?a=A'), qr/redirectA/s, 'redirect with args');
+like(http_get('/test?dest=named'), qr/named/s, 'redirect to named location');
+
+like(http_get('/test?unsafe=1'), qr/500 Internal Server/s,
+       'unsafe redirect');
+like(http_get('/test?quoted=1'), qr/200 .*redirect/s,
+       'quoted redirect');
+
+###############################################################################
diff --git a/nginx/t/js_modules.t b/nginx/t/js_modules.t
new file mode 100644 (file)
index 0000000..21581b4
--- /dev/null
@@ -0,0 +1,84 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, ES6 import, export.
+
+###############################################################################
+
+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/)
+       ->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 /test {
+            js_content test.test;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    import m from 'module.js';
+
+    function test(r) {
+        r.return(200, m[r.args.fun](r.args.a, r.args.b));
+    }
+
+    export default {test};
+
+EOF
+
+$t->write_file('module.js', <<EOF);
+    function sum(a, b) {
+        return Number(a) + Number(b);
+    }
+
+    function prod(a, b) {
+        return Number(a) * Number(b);
+    }
+
+    export default {sum, prod};
+
+EOF
+
+
+$t->try_run('no njs modules')->plan(2);
+
+###############################################################################
+
+like(http_get('/test?fun=sum&a=3&b=4'), qr/7/s, 'test sum');
+like(http_get('/test?fun=prod&a=3&b=4'), qr/12/s, 'test prod');
+
+###############################################################################
diff --git a/nginx/t/js_ngx.t b/nginx/t/js_ngx.t
new file mode 100644 (file)
index 0000000..c6271f0
--- /dev/null
@@ -0,0 +1,94 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, ngx object.
+
+###############################################################################
+
+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/)
+       ->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 /log {
+            js_content test.log;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function level(r) {
+        switch (r.args.level) {
+        case 'INFO': return ngx.INFO;
+        case 'WARN': return ngx.WARN;
+        case 'ERR': return ngx.ERR;
+        default:
+            throw Error(`Unknown log level:"\${r.args.level}"`);
+        }
+    }
+
+    function log(r) {
+        ngx.log(level(r), `ngx.log:\${r.args.text}`);
+        r.return(200);
+    }
+
+    export default {njs: test_njs, log};
+
+EOF
+
+$t->try_run('no njs ngx')->plan(3);
+
+###############################################################################
+
+http_get('/log?level=INFO&text=FOO');
+http_get('/log?level=WARN&text=BAR');
+http_get('/log?level=ERR&text=BAZ');
+
+$t->stop();
+
+like($t->read_file('error.log'), qr/\[info\].*ngx.log:FOO/, 'ngx.log info');
+like($t->read_file('error.log'), qr/\[warn\].*ngx.log:BAR/, 'ngx.log warn');
+like($t->read_file('error.log'), qr/\[error\].*ngx.log:BAZ/, 'ngx.log err');
+
+###############################################################################
diff --git a/nginx/t/js_object.t b/nginx/t/js_object.t
new file mode 100644 (file)
index 0000000..97e778a
--- /dev/null
@@ -0,0 +1,137 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, request object.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+use Socket qw/ CRLF /;
+
+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/)
+       ->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 /to_string {
+            js_content test.to_string;
+        }
+
+        location /define_prop {
+            js_content test.define_prop;
+        }
+
+        location /in_operator {
+            js_content test.in_operator;
+        }
+
+        location /redefine_bind {
+            js_content test.redefine_bind;
+        }
+
+        location /redefine_proxy {
+            js_content test.redefine_proxy;
+        }
+
+        location /redefine_proto {
+            js_content test.redefine_proto;
+        }
+
+        location /get_own_prop_descs {
+            js_content test.get_own_prop_descs;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function to_string(r) {
+        r.return(200, r.toString());
+    }
+
+    function define_prop(r) {
+        Object.defineProperty(r.headersOut, 'Foo', {value:'bar'});
+        r.return(200);
+    }
+
+    function in_operator(r) {
+        r.return(200, ['Foo', 'Bar'].map(v=>v in r.headersIn)
+                      .toString() === 'true,false');
+    }
+
+    function redefine_bind(r) {
+        r.return = r.return.bind(r, 200);
+        r.return('redefine_bind');
+    }
+
+    function redefine_proxy(r) {
+        r.return_orig = r.return;
+        r.return = function (body) { this.return_orig(200, body);}
+        r.return('redefine_proxy');
+    }
+
+    function redefine_proto(r) {
+        r[0] = 'a';
+        r[1] = 'b';
+        r.length = 2;
+        Object.setPrototypeOf(r, Array.prototype);
+        r.return(200, r.join('|'));
+    }
+
+    function get_own_prop_descs(r) {
+        r.return(200,
+                 Object.getOwnPropertyDescriptors(r)['log'].value === r.log);
+    }
+
+    export default {to_string, define_prop, in_operator, redefine_bind,
+                    redefine_proxy, redefine_proto, get_own_prop_descs};
+
+EOF
+
+$t->try_run('no njs request object')->plan(7);
+
+###############################################################################
+
+like(http_get('/to_string'), qr/\[object Request\]/, 'toString');
+like(http_get('/define_prop'), qr/Foo: bar/, 'define_prop');
+like(http(
+       'GET /in_operator HTTP/1.0' . CRLF
+       . 'Foo: foo' . CRLF
+       . 'Host: localhost' . CRLF . CRLF
+), qr/true/, 'in_operator');
+like(http_get('/redefine_bind'), qr/redefine_bind/, 'redefine_bind');
+like(http_get('/redefine_proxy'), qr/redefine_proxy/, 'redefine_proxy');
+like(http_get('/redefine_proto'), qr/a|b/, 'redefine_proto');
+like(http_get('/get_own_prop_descs'), qr/true/, 'get_own_prop_descs');
+
+###############################################################################
diff --git a/nginx/t/js_paths.t b/nginx/t/js_paths.t
new file mode 100644 (file)
index 0000000..98d8751
--- /dev/null
@@ -0,0 +1,110 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, js_path directive.
+
+###############################################################################
+
+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/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    js_path "%%TESTDIR%%/lib1";
+    js_path "lib2";
+
+    js_import test.js;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /test {
+            js_content test.test;
+        }
+
+        location /test2 {
+            js_content test.test2;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    import m1 from 'module1.js';
+    import m2 from 'module2.js';
+    import m3 from 'lib1/module1.js';
+
+    function test(r) {
+        r.return(200, m1[r.args.fun](r.args.a, r.args.b));
+    }
+
+    function test2(r) {
+        r.return(200, m2.sum(r.args.a, r.args.b));
+    }
+
+    function test3(r) {
+        r.return(200, m3.sum(r.args.a, r.args.b));
+    }
+
+    export default {test, test2};
+
+EOF
+
+my $d = $t->testdir();
+
+mkdir("$d/lib1");
+mkdir("$d/lib2");
+
+$t->write_file('lib1/module1.js', <<EOF);
+    function sum(a, b) { return Number(a) + Number(b); }
+    function prod(a, b) { return Number(a) * Number(b); }
+
+    export default {sum, prod};
+
+EOF
+
+$t->write_file('lib2/module2.js', <<EOF);
+    function sum(a, b) { return a + b; }
+
+    export default {sum};
+
+EOF
+
+
+$t->try_run('no njs available')->plan(4);
+
+###############################################################################
+
+like(http_get('/test?fun=sum&a=3&b=4'), qr/7/s, 'test sum');
+like(http_get('/test?fun=prod&a=3&b=4'), qr/12/s, 'test prod');
+like(http_get('/test2?a=3&b=4'), qr/34/s, 'test2');
+like(http_get('/test2?a=A&b=B'), qr/AB/s, 'test2 relative');
+
+###############################################################################
diff --git a/nginx/t/js_preload_object.t b/nginx/t/js_preload_object.t
new file mode 100644 (file)
index 0000000..407e97f
--- /dev/null
@@ -0,0 +1,181 @@
+#!/usr/bin/perl
+
+# (C) Vadim Zhestikov
+# (C) Nginx, Inc.
+
+# Tests for http njs module, js_preload_object directive.
+
+###############################################################################
+
+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_preload_object g1 from g.json;
+    js_preload_object ga from ga.json;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        js_import lib.js;
+        js_preload_object lx from l.json;
+
+        location /test {
+            js_content lib.test;
+        }
+
+        location /test_query {
+            js_import lib1.js;
+            js_content lib1.query;
+        }
+
+        location /test_query_preloaded {
+            js_import lib1.js;
+            js_preload_object l.json;
+            js_content lib1.query;
+        }
+
+        location /test_var {
+            js_set $test_var lib.test_var;
+            return 200 $test_var;
+        }
+
+        location /test_mutate {
+            js_content lib.mutate;
+        }
+
+        location /test_no_suffix {
+            js_preload_object gg from no_suffix;
+            js_content lib.suffix;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('lib.js', <<EOF);
+    function test(r) {
+        r.return(200, ga + ' ' + g1.c.prop[0].a + ' ' + lx);
+    }
+
+    function test_var(r) {
+        return g1.b[2];
+    }
+
+    function mutate(r) {
+        var res = "OK";
+
+        try {
+            switch (r.args.method) {
+            case 'set_obj':
+                g1.c.prop[0].a = 5;
+                break;
+            case 'set_arr':
+                g1.c.prop[0] = 5;
+                break;
+            case 'add_obj':
+                g1.c.prop[0].xxx = 5;
+                break;
+            case 'add_arr':
+                g1.c.prop[10] = 5;
+                break;
+            case 'del_obj':
+                delete g1.c.prop[0].a;
+                break;
+            case 'del_arr':
+                delete g1.c.prop[0];
+                break;
+            }
+
+        } catch (e) {
+            res = e.message;
+        }
+
+        r.return(200, res);
+    }
+
+    function suffix(r) {
+        r.return(200, gg);
+    }
+
+    export default {test, test_var, mutate, suffix};
+
+EOF
+
+$t->write_file('lib1.js', <<EOF);
+    function query(r) {
+        var res = 'ok';
+
+        try {
+            res = r.args.path.split('.').reduce((a, v) => a[v], globalThis);
+
+        } catch (e) {
+            res = e.message;
+        }
+
+        r.return(200, njs.dump(res));
+    }
+
+    export default {query};
+
+EOF
+
+$t->write_file('g.json',
+       '{"a":1, "b":[1,2,"element",4,5], "c":{"prop":[{"a":2}]}}');
+$t->write_file('ga.json', '"ga loaded"');
+$t->write_file('l.json', '"l loaded"');
+$t->write_file('no_suffix', '"no_suffix loaded"');
+
+$t->try_run('no js_preload_object available')->plan(12);
+
+###############################################################################
+
+like(http_get('/test'), qr/ga loaded 2 l loaded/s, 'direct query');
+like(http_get('/test_query?path=l'), qr/undefined/s, 'unreferenced');
+like(http_get('/test_query_preloaded?path=l'), qr/l loaded/s,
+       'reference preload');
+like(http_get('/test_query?path=g1.b.1'), qr/2/s, 'complex query');
+like(http_get('/test_var'), qr/element/s, 'var reference');
+
+like(http_get('/test_mutate?method=set_obj'), qr/Cannot assign to read-only/s,
+       'preload_object props are const (object)');
+like(http_get('/test_mutate?method=set_arr'), qr/Cannot assign to read-only/s,
+       'preload_object props are const (array)');
+like(http_get('/test_mutate?method=add_obj'), qr/Cannot add property "xxx"/s,
+       'preload_object props are not extensible (object)');
+like(http_get('/test_mutate?method=add_arr'), qr/Cannot add property "10"/s,
+       'preload_object props are not extensible (array)');
+like(http_get('/test_mutate?method=del_obj'), qr/Cannot delete property "a"/s,
+       'preload_object props are not deletable (object)');
+like(http_get('/test_mutate?method=del_arr'), qr/Cannot delete property "0"/s,
+       'preload_object props are not deletable (array)');
+
+like(http_get('/test_no_suffix'), qr/no_suffix loaded/s,
+       'load without suffix');
+
+###############################################################################
diff --git a/nginx/t/js_promise.t b/nginx/t/js_promise.t
new file mode 100644 (file)
index 0000000..f6084e9
--- /dev/null
@@ -0,0 +1,201 @@
+#!/usr/bin/perl
+
+# (C) Nginx, Inc.
+
+# Promise tests for http njs module.
+
+###############################################################################
+
+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/)
+       ->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 /promise {
+            js_content test.promise;
+        }
+
+        location /promise_throw {
+            js_content test.promise_throw;
+        }
+
+        location /promise_pure {
+            js_content test.promise_pure;
+        }
+
+        location /timeout {
+            js_content test.timeout;
+        }
+
+        location /sub_token {
+            js_content test.sub_token;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    var global_token = '';
+
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function promise(r) {
+        promisified_subrequest(r, '/sub_token', 'code=200&token=a')
+        .then(reply => {
+            var data = JSON.parse(reply.responseText);
+
+            if (data['token'] !== "a") {
+                throw new Error('token is not "a"');
+            }
+
+            return data['token'];
+        })
+        .then(token => {
+            promisified_subrequest(r, '/sub_token', 'code=200&token=b')
+            .then(reply => {
+                var data = JSON.parse(reply.responseText);
+
+                r.return(200, '{"token": "' + data['token'] + '"}');
+            })
+            .catch(() => {
+                throw new Error("failed promise() test");
+            });
+        })
+        .catch(() => {
+            r.return(500);
+        });
+    }
+
+    function promise_throw(r) {
+        promisified_subrequest(r, '/sub_token', 'code=200&token=x')
+        .then(reply => {
+            var data = JSON.parse(reply.responseText);
+
+            if (data['token'] !== "a") {
+                throw data['token'];
+            }
+
+            return data['token'];
+        })
+        .then(() => {
+            r.return(500);
+        })
+        .catch(token => {
+            r.return(200, '{"token": "' + token + '"}');
+        });
+    }
+
+    function promise_pure(r) {
+        var count = 0;
+
+        Promise.resolve(true)
+        .then(() => count++)
+        .then(() => not_exist_ref)
+        .finally(() => count++)
+        .catch(() => {
+            r.return((count != 2) ? 500 : 200);
+        });
+    }
+
+    function timeout(r) {
+        promisified_subrequest(r, '/sub_token', 'code=200&token=R')
+        .then(reply => JSON.parse(reply.responseText))
+        .then(data => {
+            setTimeout(timeout_cb, 50, r, '/sub_token', 'code=200&token=T');
+            return data;
+        })
+        .then(data => {
+            setTimeout(timeout_cb, 1, r, '/sub_token', 'code=200&token='
+                                                        + data['token']);
+        })
+        .catch(() => {
+            r.return(500);
+        });
+    }
+
+    function timeout_cb(r, url, args) {
+        promisified_subrequest(r, url, args)
+        .then(reply => {
+            if (global_token == '') {
+                var data = JSON.parse(reply.responseText);
+
+                global_token = data['token'];
+
+                r.return(200, '{"token": "' + data['token'] + '"}');
+            }
+        })
+        .catch(() => {
+            r.return(500);
+        });
+    }
+
+    function promisified_subrequest(r, uri, args) {
+        return new Promise((resolve, reject) => {
+            r.subrequest(uri, args, (reply) => {
+                if (reply.status < 400) {
+                    resolve(reply);
+                } else {
+                    reject(reply);
+                }
+            });
+        })
+    }
+
+    function sub_token(r) {
+        var code = r.variables.arg_code;
+        var token = r.variables.arg_token;
+
+        r.return(parseInt(code), '{"token": "'+ token +'"}');
+    }
+
+    export default {njs:test_njs, promise, promise_throw, promise_pure,
+                    timeout, sub_token};
+
+EOF
+
+$t->try_run('no njs available')->plan(4);
+
+###############################################################################
+
+like(http_get('/promise'), qr/{"token": "b"}/, "Promise");
+like(http_get('/promise_throw'), qr/{"token": "x"}/, "Promise throw and catch");
+like(http_get('/timeout'), qr/{"token": "R"}/, "Promise with timeout");
+like(http_get('/promise_pure'), qr/200 OK/, "events handling");
+
+###############################################################################
diff --git a/nginx/t/js_request_body.t b/nginx/t/js_request_body.t
new file mode 100644 (file)
index 0000000..350e7fb
--- /dev/null
@@ -0,0 +1,110 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, r.requestText method.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+use Socket qw/ CRLF /;
+
+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/)
+       ->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 /body {
+            js_content test.body;
+        }
+
+        location /in_file {
+            client_body_in_file_only on;
+            js_content test.body;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function body(r) {
+        try {
+            var body = r.requestText;
+            r.return(200, body);
+
+        } catch (e) {
+            r.return(500, e.message);
+        }
+    }
+
+    export default {body};
+
+EOF
+
+$t->try_run('no njs request body')->plan(3);
+
+###############################################################################
+
+like(http_post('/body'), qr/REQ-BODY/, 'request body');
+like(http_post('/in_file'), qr/request body is in a file/,
+       'request body in file');
+like(http_post_big('/body'), qr/200.*^(1234567890){1024}$/ms,
+               'request body big');
+
+###############################################################################
+
+sub http_post {
+       my ($url, %extra) = @_;
+
+       my $p = "POST $url HTTP/1.0" . CRLF .
+               "Host: localhost" . CRLF .
+               "Content-Length: 8" . CRLF .
+               CRLF .
+               "REQ-BODY";
+
+       return http($p, %extra);
+}
+
+sub http_post_big {
+       my ($url, %extra) = @_;
+
+       my $p = "POST $url HTTP/1.0" . CRLF .
+               "Host: localhost" . CRLF .
+               "Content-Length: 10240" . CRLF .
+               CRLF .
+               ("1234567890" x 1024);
+
+       return http($p, %extra);
+}
+
+###############################################################################
diff --git a/nginx/t/js_return.t b/nginx/t/js_return.t
new file mode 100644 (file)
index 0000000..2cc32f7
--- /dev/null
@@ -0,0 +1,73 @@
+#!/usr/bin/perl
+
+# (C) Sergey Kandaurov
+# (C) Nginx, Inc.
+
+# Tests for http njs module, return method.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+use Config;
+
+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/)
+       ->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 / {
+            js_content test.returnf;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function returnf(r) {
+        r.return(Number(r.args.c), r.args.t);
+    }
+
+    export default {returnf};
+
+EOF
+
+$t->try_run('no njs return')->plan(5);
+
+###############################################################################
+
+like(http_get('/?c=200'), qr/200 OK.*\x0d\x0a?\x0d\x0a?$/s, 'return code');
+like(http_get('/?c=200&t=SEE-THIS'), qr/200 OK.*^SEE-THIS$/ms, 'return text');
+like(http_get('/?c=301&t=path'), qr/ 301 .*Location: path/s, 'return redirect');
+like(http_get('/?c=404'), qr/404 Not.*html/s, 'return error page');
+like(http_get('/?c=inv'), qr/ 500 /, 'return invalid');
+
+###############################################################################
diff --git a/nginx/t/js_subrequests.t b/nginx/t/js_subrequests.t
new file mode 100644 (file)
index 0000000..a2007fa
--- /dev/null
@@ -0,0 +1,636 @@
+#!/usr/bin/perl
+#
+# (C) Dmitry Volyntsev.
+# (C) Nginx, Inc.
+
+# Tests for subrequests in http njs module.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+use Socket qw/ CRLF /;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+eval { require JSON::PP; };
+plan(skip_all => "JSON::PP not installed") if $@;
+
+my $t = Test::Nginx->new()->has(qw/http rewrite proxy cache/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    proxy_cache_path   %%TESTDIR%%/cache1
+                       keys_zone=ON:1m      use_temp_path=on;
+
+    js_import test.js;
+
+    js_set $async_var       test.async_var;
+    js_set $subrequest_var  test.subrequest_var;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /njs {
+            js_content test.njs;
+        }
+
+        location /sr {
+            js_content test.sr;
+        }
+
+        location /sr_pr {
+            js_content test.sr_pr;
+        }
+
+        location /sr_args {
+            js_content test.sr_args;
+        }
+
+        location /sr_options_args {
+            js_content test.sr_options_args;
+        }
+
+        location /sr_options_args_pr {
+            js_content test.sr_options_args_pr;
+        }
+
+        location /sr_options_method {
+            js_content test.sr_options_method;
+        }
+
+        location /sr_options_method_pr {
+            js_content test.sr_options_method_pr;
+        }
+
+        location /sr_options_body {
+            js_content test.sr_options_body;
+        }
+
+        location /sr_options_method_head {
+            js_content test.sr_options_method_head;
+        }
+
+        location /sr_body {
+            js_content test.sr_body;
+        }
+
+        location /sr_body_pr {
+            js_content test.sr_body_pr;
+        }
+
+        location /sr_body_special {
+            js_content test.sr_body_special;
+        }
+
+        location /sr_in_variable_handler {
+            set $_ $async_var;
+            js_content test.sr_in_variable_handler;
+        }
+
+        location /sr_detached_in_variable_handler {
+            return 200 $subrequest_var;
+        }
+
+        location /sr_async_var {
+            set $_ $async_var;
+            error_page 404 /return;
+            return 404;
+        }
+
+        location /sr_error_page {
+            js_content test.sr_error_page;
+        }
+
+        location /sr_js_in_subrequest {
+            js_content test.sr_js_in_subrequest;
+        }
+
+        location /sr_js_in_subrequest_pr {
+            js_content test.sr_js_in_subrequest_pr;
+        }
+
+        location /sr_file {
+            js_content test.sr_file;
+        }
+
+        location /sr_cache {
+            js_content test.sr_cache;
+        }
+
+
+        location /sr_unavail {
+            js_content test.sr_unavail;
+        }
+
+        location /sr_unavail_pr {
+            js_content test.sr_unavail_pr;
+        }
+
+        location /sr_broken {
+            js_content test.sr_broken;
+        }
+
+        location /sr_too_large {
+            js_content test.sr_too_large;
+        }
+
+        location /sr_out_of_order {
+            js_content test.sr_out_of_order;
+        }
+
+        location /sr_except_not_a_func {
+            js_content test.sr_except_not_a_func;
+        }
+
+        location /sr_except_failed_to_convert_options_arg {
+            js_content test.sr_except_failed_to_convert_options_arg;
+        }
+
+        location /sr_except_invalid_options_header_only {
+            js_content test.sr_except_invalid_options_header_only;
+        }
+
+        location /sr_in_sr_callback {
+            js_content test.sr_in_sr_callback;
+        }
+
+        location /sr_uri_except {
+            js_content test.sr_uri_except;
+        }
+
+
+        location /file/ {
+            alias %%TESTDIR%%/;
+        }
+
+        location /p/ {
+            proxy_cache $arg_c;
+            proxy_pass http://127.0.0.1:8081/;
+        }
+
+        location /daemon/ {
+            proxy_pass http://127.0.0.1:8082/;
+        }
+
+        location /too_large/ {
+            subrequest_output_buffer_size 3;
+            proxy_pass http://127.0.0.1:8081/;
+        }
+
+        location /sr_in_sr {
+            js_content test.sr_in_sr;
+        }
+
+        location /unavail {
+            proxy_pass http://127.0.0.1:8084/;
+        }
+
+        location /sr_parent {
+             js_content test.sr_parent;
+        }
+
+        location /js_sub {
+            js_content test.js_sub;
+        }
+
+        location /return {
+            return 200 '["$request_method"]';
+        }
+
+        location /error_page_404 {
+            return 404;
+
+            error_page 404 /404.html;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8081;
+        server_name  localhost;
+
+        location /sub1 {
+            add_header H $arg_h;
+            return 206 '{"a": {"b": 1}}';
+        }
+
+        location /sub2 {
+            return 404 '{"e": "msg"}';
+        }
+
+        location /method {
+            return 200 '["$request_method"]';
+        }
+
+        location /body {
+            js_content test.body;
+        }
+
+        location /detached {
+            js_content test.detached;
+        }
+
+        location /delayed {
+            js_content test.delayed;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8084;
+        server_name  localhost;
+
+        return 444;
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function sr(r) {
+        subrequest_fn(r, ['/p/sub2'], ['uri', 'status'])
+    }
+
+    function sr_pr(r) {
+        r.subrequest('/p/sub1', 'h=xxx')
+        .then(reply => r.return(200, JSON.stringify({h:reply.headersOut.h})))
+    }
+
+    function sr_args(r) {
+        r.subrequest('/p/sub1', 'h=xxx', reply => {
+            r.return(200, JSON.stringify({h:reply.headersOut.h}));
+        });
+    }
+
+    function sr_options_args(r) {
+        r.subrequest('/p/sub1', {args:'h=xxx'}, reply => {
+            r.return(200, JSON.stringify({h:reply.headersOut.h}));
+        });
+    }
+
+    function sr_options_args_pr(r) {
+        r.subrequest('/p/sub1', {args:'h=xxx'})
+        .then(reply => r.return(200, JSON.stringify({h:reply.headersOut.h})))
+    }
+
+    function sr_options_method(r) {
+        r.subrequest('/p/method', {method:r.args.m}, body_fwd_cb);
+    }
+
+    function sr_options_method_pr(r) {
+        r.subrequest('/p/method', {method:r.args.m})
+        .then(body_fwd_cb);
+    }
+
+    function sr_options_body(r) {
+        r.subrequest('/p/body', {method:'POST', body:'["REQ-BODY"]'},
+                     body_fwd_cb);
+    }
+
+    function sr_options_method_head(r) {
+        r.subrequest('/p/method', {method:'HEAD'}, reply => {
+            r.return(200, JSON.stringify({c:reply.status}));
+        });
+    }
+
+    function sr_body(r) {
+        r.subrequest('/p/sub1', body_fwd_cb);
+    }
+
+    function sr_body_pr(r) {
+        r.subrequest('/p/sub1')
+        .then(body_fwd_cb);
+    }
+
+    function sr_body_special(r) {
+        r.subrequest('/p/sub2', body_fwd_cb);
+    }
+
+    function body(r) {
+        r.return(200, r.variables.request_body);
+    }
+
+    function delayed(r) {
+        setTimeout(r => r.return(200), 100, r);
+    }
+
+    function detached(r) {
+        var method = r.variables.request_method;
+        r.log(`DETACHED: \${method} args: \${r.variables.args}`);
+
+        r.return(200);
+    }
+
+    function sr_in_variable_handler(r) {
+    }
+
+    function async_var(r) {
+        r.subrequest('/p/delayed', reply => {
+            r.return(200, JSON.stringify(["CB-VAR"]));
+        });
+
+        return "";
+    }
+
+    function sr_error_page(r) {
+         r.subrequest('/error_page_404')
+         .then(reply => {r.return(200, `reply.status:\${reply.status}`)});
+    }
+
+    function subrequest_var(r) {
+        r.subrequest('/p/detached',  {detached:true});
+        r.subrequest('/p/detached',  {detached:true, args:'a=yyy',
+                                      method:'POST'});
+
+        return "subrequest_var";
+    }
+
+    function sr_file(r) {
+        r.subrequest('/file/t', body_fwd_cb);
+    }
+
+    function sr_cache(r) {
+        r.subrequest('/p/t', body_fwd_cb);
+    }
+
+    function sr_unavail(req) {
+        subrequest_fn(req, ['/unavail'], ['uri', 'status']);
+    }
+
+    function sr_unavail_pr(req) {
+        subrequest_fn_pr(req, ['/unavail'], ['uri', 'status']);
+    }
+
+    function sr_broken(r) {
+        r.subrequest('/daemon/unfinished', reply => {
+            r.return(200, JSON.stringify({code:reply.status}));
+        });
+    }
+
+    function sr_too_large(r) {
+        r.subrequest('/too_large/t', body_fwd_cb);
+    }
+
+    function sr_in_sr(r) {
+        r.subrequest('/sr', body_fwd_cb);
+    }
+
+    function sr_js_in_subrequest(r) {
+        r.subrequest('/js_sub', body_fwd_cb);
+    }
+
+    function sr_js_in_subrequest_pr(r) {
+        r.subrequest('/js_sub')
+        .then(body_fwd_cb);
+    }
+
+    function sr_in_sr_callback(r) {
+        r.subrequest('/return', function (reply) {
+                try {
+                    reply.subrequest('/return');
+
+                } catch (err) {
+                    r.return(200, JSON.stringify({e:err.message}));
+                    return;
+                }
+
+                r.return(200);
+            });
+    }
+
+    function sr_parent(r) {
+        try {
+            var parent = r.parent;
+
+        } catch (err) {
+            r.return(200, JSON.stringify({e:err.message}));
+            return;
+        }
+
+        r.return(200);
+    }
+
+    function sr_out_of_order(r) {
+        subrequest_fn(r, ['/p/delayed', '/p/sub1', '/unknown'],
+                      ['uri', 'status']);
+    }
+
+    function collect(replies, props, total, reply) {
+        reply.log(`subrequest handler: \${reply.uri} status: \${reply.status}`)
+
+        var rep = {};
+        props.forEach(p => {rep[p] = reply[p]});
+
+        replies.push(rep);
+
+        if (replies.length == total) {
+            reply.parent.return(200, JSON.stringify(replies));
+        }
+    }
+
+    function subrequest_fn(r, subs, props) {
+        var replies = [];
+
+        subs.forEach(sr =>
+                     r.subrequest(sr, collect.bind(null, replies,
+                                                   props, subs.length)));
+    }
+
+    function subrequest_fn_pr(r, subs, props) {
+        var replies = [];
+
+        subs.forEach(sr => r.subrequest(sr)
+            .then(collect.bind(null, replies, props, subs.length)));
+    }
+
+    function sr_except_not_a_func(r) {
+        r.subrequest('/sub1', 'a=1', 'b');
+    }
+
+    let Failed = {get toConvert() { return {toString(){return {};}}}};
+
+    function sr_except_failed_to_convert_options_arg(r) {
+        r.subrequest('/sub1', {args:Failed.toConvert}, ()=>{});
+    }
+
+    function sr_uri_except(r) {
+        r.subrequest(Failed.toConvert, 'a=1', 'b');
+    }
+
+    function body_fwd_cb(r) {
+        r.parent.return(200, JSON.stringify(JSON.parse(r.responseText)));
+    }
+
+    function js_sub(r) {
+        r.return(200, '["JS-SUB"]');
+    }
+
+    export default {njs:test_njs, sr, sr_pr, sr_args, sr_options_args,
+                    sr_options_args_pr, sr_options_method, sr_options_method_pr,
+                    sr_options_method_head, sr_options_body, sr_body,
+                    sr_body_pr, sr_body_special, body, delayed, detached,
+                    sr_in_variable_handler, async_var, sr_error_page,
+                    subrequest_var, sr_file, sr_cache, sr_unavail, sr_parent,
+                    sr_unavail_pr, sr_broken, sr_too_large, sr_in_sr,
+                    sr_js_in_subrequest, sr_js_in_subrequest_pr, js_sub,
+                    sr_in_sr_callback, sr_out_of_order, sr_except_not_a_func,
+                    sr_uri_except, sr_except_failed_to_convert_options_arg};
+
+EOF
+
+$t->write_file('t', '["SEE-THIS"]');
+
+$t->try_run('no njs available')->plan(32);
+$t->run_daemon(\&http_daemon);
+
+###############################################################################
+
+is(get_json('/sr'), '[{"status":404,"uri":"/p/sub2"}]', 'sr');
+is(get_json('/sr_args'), '{"h":"xxx"}', 'sr_args');
+is(get_json('/sr_options_args'), '{"h":"xxx"}', 'sr_options_args');
+is(get_json('/sr_options_method?m=POST'), '["POST"]', 'sr method POST');
+is(get_json('/sr_options_method?m=PURGE'), '["PURGE"]', 'sr method PURGE');
+is(get_json('/sr_options_body'), '["REQ-BODY"]', 'sr_options_body');
+is(get_json('/sr_options_method_head'), '{"c":200}', 'sr_options_method_head');
+is(get_json('/sr_body'), '{"a":{"b":1}}', 'sr_body');
+is(get_json('/sr_body_special'), '{"e":"msg"}', 'sr_body_special');
+is(get_json('/sr_in_variable_handler'), '["CB-VAR"]', 'sr_in_variable_handler');
+is(get_json('/sr_file'), '["SEE-THIS"]', 'sr_file');
+is(get_json('/sr_cache?c=1'), '["SEE-THIS"]', 'sr_cache');
+is(get_json('/sr_cache?c=1'), '["SEE-THIS"]', 'sr_cached');
+is(get_json('/sr_js_in_subrequest'), '["JS-SUB"]', 'sr_js_in_subrequest');
+is(get_json('/sr_unavail'), '[{"status":502,"uri":"/unavail"}]',
+       'sr_unavail');
+is(get_json('/sr_out_of_order'),
+       '[{"status":404,"uri":"/unknown"},' .
+       '{"status":206,"uri":"/p/sub1"},' .
+       '{"status":200,"uri":"/p/delayed"}]',
+       'sr_multi');
+
+is(get_json('/sr_pr'), '{"h":"xxx"}', 'sr_promise');
+is(get_json('/sr_options_args_pr'), '{"h":"xxx"}', 'sr_options_args_pr');
+is(get_json('/sr_options_method_pr?m=PUT'), '["PUT"]', 'sr method PUT');
+is(get_json('/sr_body_pr'), '{"a":{"b":1}}', 'sr_body_pr');
+is(get_json('/sr_js_in_subrequest_pr'), '["JS-SUB"]', 'sr_js_in_subrequest_pr');
+is(get_json('/sr_unavail_pr'), '[{"status":502,"uri":"/unavail"}]',
+       'sr_unavail_pr');
+
+like(http_get('/sr_detached_in_variable_handler'), qr/subrequest_var/,
+     'sr_detached_in_variable_handler');
+
+like(http_get('/sr_error_page'), qr/reply\.status:404/,
+     'sr_error_page');
+
+http_get('/sr_broken');
+http_get('/sr_in_sr');
+http_get('/sr_in_variable_handler');
+http_get('/sr_async_var');
+http_get('/sr_too_large');
+http_get('/sr_except_not_a_func');
+http_get('/sr_except_failed_to_convert_options_arg');
+http_get('/sr_uri_except');
+
+is(get_json('/sr_in_sr_callback'),
+       '{"e":"subrequest can only be created for the primary request"}',
+       'subrequest for non-primary request');
+
+$t->stop();
+
+ok(index($t->read_file('error.log'), 'callback is not a function') > 0,
+       'subrequest cb exception');
+ok(index($t->read_file('error.log'), 'failed to convert uri arg') > 0,
+       'subrequest uri exception');
+ok(index($t->read_file('error.log'), 'failed to convert options.args') > 0,
+       'subrequest invalid args exception');
+ok(index($t->read_file('error.log'), 'too big subrequest response') > 0,
+       'subrequest too large body');
+ok(index($t->read_file('error.log'), 'subrequest creation failed') > 0,
+       'subrequest creation failed');
+ok(index($t->read_file('error.log'),
+               'js subrequest: failed to get the parent context') > 0,
+       'zero parent ctx');
+
+ok(index($t->read_file('error.log'), 'DETACHED') > 0,
+       'detached subrequest');
+
+###############################################################################
+
+sub recode {
+       my $json;
+       eval { $json = JSON::PP::decode_json(shift) };
+
+       if ($@) {
+               return "<failed to parse JSON>";
+       }
+
+       JSON::PP->new()->canonical()->encode($json);
+}
+
+sub get_json {
+       http_get(shift) =~ /\x0d\x0a?\x0d\x0a?(.*)/ms;
+       recode($1);
+}
+
+###############################################################################
+
+sub http_daemon {
+       my $server = IO::Socket::INET->new(
+               Proto => 'tcp',
+               LocalAddr => '127.0.0.1:' . port(8082),
+               Listen => 5,
+               Reuse => 1
+       )
+               or die "Can't create listening socket: $!\n";
+
+       local $SIG{PIPE} = 'IGNORE';
+
+       while (my $client = $server->accept()) {
+               $client->autoflush(1);
+
+               my $headers = '';
+               my $uri = '';
+
+               while (<$client>) {
+                       $headers .= $_;
+                       last if (/^\x0d?\x0a?$/);
+               }
+
+               $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i;
+
+               if ($uri eq '/unfinished') {
+                       print $client
+                               "HTTP/1.1 200 OK" . CRLF .
+                               "Transfer-Encoding: chunked" . CRLF .
+                               "Content-Length: 100" . CRLF .
+                               CRLF .
+                               "unfinished" . CRLF;
+                       close($client);
+               }
+       }
+}
+
+###############################################################################
diff --git a/nginx/t/js_var.t b/nginx/t/js_var.t
new file mode 100644 (file)
index 0000000..4de8236
--- /dev/null
@@ -0,0 +1,90 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, js_var directive.
+
+###############################################################################
+
+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;
+
+    js_var $foo;
+    js_var $bar a:$arg_a;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /test {
+            js_content test.test;
+        }
+
+        location /sub {
+            return 200 DONE;
+        }
+
+        location /dest {
+            return 200 $bar;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test(r) {
+        if (r.args.sub) {
+            r.subrequest('/sub')
+            .then(reply => {
+                r.variables.bar = reply.responseText;
+                r.internalRedirect('/dest');
+            });
+
+            return;
+        }
+
+        r.return(200, `V:\${r.variables[r.args.var]}`);
+    }
+
+    export default {test};
+
+EOF
+
+$t->try_run('no njs js_var')->plan(3);
+
+###############################################################################
+
+like(http_get('/test?var=bar&a=qq'), qr/200 OK.*V:a:qq$/s, 'default value');
+like(http_get('/test?var=foo'), qr/200 OK.*V:$/s, 'default empty value');
+like(http_get('/test?sub=1&var=bar&a=qq'), qr/200 OK.*DONE$/s, 'value set');
+
+###############################################################################
diff --git a/nginx/t/js_var2.t b/nginx/t/js_var2.t
new file mode 100644 (file)
index 0000000..5c59d9c
--- /dev/null
@@ -0,0 +1,90 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, js_var directive in server | location contexts.
+
+###############################################################################
+
+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;
+
+        js_var $foo;
+
+        location /test {
+            js_content test.test;
+        }
+
+        location /sub {
+            return 200 DONE;
+        }
+
+        location /dest {
+            js_var $bar a:$arg_a;
+            return 200 $bar;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test(r) {
+        if (r.args.sub) {
+            r.subrequest('/sub')
+            .then(reply => {
+                r.variables.bar = reply.responseText;
+                r.internalRedirect('/dest');
+            });
+
+            return;
+        }
+
+        r.return(200, `V:\${r.variables[r.args.var]}`);
+    }
+
+    export default {test};
+
+EOF
+
+$t->try_run('no njs js_var')->plan(3);
+
+###############################################################################
+
+like(http_get('/test?var=bar&a=qq'), qr/200 OK.*V:a:qq$/s, 'default value');
+like(http_get('/test?var=foo'), qr/200 OK.*V:$/s, 'default empty value');
+like(http_get('/test?sub=1&var=bar&a=qq'), qr/200 OK.*DONE$/s, 'value set');
+
+###############################################################################
diff --git a/nginx/t/js_variables.t b/nginx/t/js_variables.t
new file mode 100644 (file)
index 0000000..f2481e0
--- /dev/null
@@ -0,0 +1,95 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for http njs module, setting nginx variables.
+
+###############################################################################
+
+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_set $test_var   test.variable;
+
+    js_import test.js;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        set $foo       test.foo_orig;
+
+        location /var_set {
+            return 200 $test_var$foo;
+        }
+
+        location /content_set {
+            js_content test.content_set;
+        }
+
+        location /not_found_set {
+            js_content test.not_found_set;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function variable(r) {
+        r.variables.foo = r.variables.arg_a;
+        return 'test_var';
+    }
+
+    function content_set(r) {
+        r.variables.foo = r.variables.arg_a;
+        r.return(200, r.variables.foo);
+    }
+
+    function not_found_set(r) {
+        try {
+            r.variables.unknown = 1;
+        } catch (e) {
+            r.return(500, e);
+        }
+    }
+
+    export default {variable, content_set, not_found_set};
+
+EOF
+
+$t->try_run('no njs')->plan(3);
+
+###############################################################################
+
+like(http_get('/var_set?a=bar'), qr/test_varbar/, 'var set');
+like(http_get('/content_set?a=bar'), qr/bar/, 'content set');
+like(http_get('/not_found_set'), qr/variable not found/, 'not found exception');
+
+###############################################################################
diff --git a/nginx/t/stream_js.t b/nginx/t/stream_js.t
new file mode 100644 (file)
index 0000000..ff92aad
--- /dev/null
@@ -0,0 +1,478 @@
+#!/usr/bin/perl
+
+# (C) Andrey Zelenkov
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for stream njs module.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ dgram stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http proxy rewrite stream stream_return udp/)
+       ->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:8079;
+        server_name  localhost;
+
+        location /njs {
+            js_content test.njs;
+        }
+
+        location /p/ {
+            proxy_pass http://127.0.0.1:8095/;
+
+        }
+
+        location /return {
+            return 200 $http_foo;
+        }
+    }
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_set $js_addr      test.addr;
+    js_set $js_var       test.variable;
+    js_set $js_log       test.log;
+    js_set $js_unk       test.unk;
+    js_set $js_req_line  test.req_line;
+    js_set $js_sess_unk  test.sess_unk;
+    js_set $js_async     test.asyncf;
+
+    js_import test.js;
+
+    log_format status $server_port:$status;
+
+    server {
+        listen  127.0.0.1:8080;
+        return  $js_addr;
+    }
+
+    server {
+        listen  127.0.0.1:8081;
+        return  $js_log;
+    }
+
+    server {
+        listen  127.0.0.1:8082;
+        return  $js_var;
+    }
+
+    server {
+        listen  127.0.0.1:8083;
+        return  $js_unk;
+    }
+
+    server {
+        listen  127.0.0.1:8084;
+        return  $js_sess_unk;
+    }
+
+    server {
+        listen  127.0.0.1:%%PORT_8985_UDP%% udp;
+        return  $js_addr;
+    }
+
+    server {
+        listen      127.0.0.1:8086;
+        js_access   test.access_step;
+        js_preread  test.preread_step;
+        js_filter   test.filter_step;
+        proxy_pass  127.0.0.1:8090;
+    }
+
+    server {
+        listen      127.0.0.1:8087;
+        js_access   test.access_undecided;
+        return      OK;
+        access_log  %%TESTDIR%%/status.log status;
+    }
+
+    server {
+        listen      127.0.0.1:8088;
+        js_access   test.access_allow;
+        return      OK;
+        access_log  %%TESTDIR%%/status.log status;
+    }
+
+    server {
+        listen      127.0.0.1:8089;
+        js_access   test.access_deny;
+        return      OK;
+        access_log  %%TESTDIR%%/status.log status;
+    }
+
+    server {
+        listen      127.0.0.1:8091;
+        js_preread  test.preread_async;
+        proxy_pass  127.0.0.1:8090;
+    }
+
+    server {
+        listen      127.0.0.1:8092;
+        js_preread  test.preread_data;
+        proxy_pass  127.0.0.1:8090;
+    }
+
+    server {
+        listen      127.0.0.1:8093;
+        js_preread  test.preread_req_line;
+        return      $js_req_line;
+    }
+
+    server {
+        listen      127.0.0.1:8094;
+        js_filter   test.filter_empty;
+        proxy_pass  127.0.0.1:8090;
+    }
+
+    server {
+        listen      127.0.0.1:8095;
+        js_filter   test.filter_header_inject;
+        proxy_pass  127.0.0.1:8079;
+    }
+
+    server {
+        listen      127.0.0.1:8096;
+        js_filter   test.filter_search;
+        proxy_pass  127.0.0.1:8090;
+    }
+
+    server {
+        listen      127.0.0.1:8097;
+        js_access   test.access_except;
+        proxy_pass  127.0.0.1:8090;
+    }
+
+    server {
+        listen      127.0.0.1:8098;
+        js_preread  test.preread_except;
+        proxy_pass  127.0.0.1:8090;
+    }
+
+    server {
+        listen      127.0.0.1:8099;
+        js_filter   test.filter_except;
+        proxy_pass  127.0.0.1:8090;
+    }
+
+    server {
+        listen      127.0.0.1:8100;
+        return      $js_async;
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function addr(s) {
+        return 'addr=' + s.remoteAddress;
+    }
+
+    function variable(s) {
+        return 'variable=' + s.variables.remote_addr;
+    }
+
+    function sess_unk(s) {
+        return 'sess_unk=' + s.unk;
+    }
+
+    function log(s) {
+        s.log("SEE-THIS");
+    }
+
+    var res = '';
+
+    function access_step(s) {
+        res += '1';
+
+        setTimeout(function() {
+            if (s.remoteAddress.match('127.0.0.1')) {
+                s.allow();
+            }
+        }, 1);
+    }
+
+    function preread_step(s) {
+        s.on('upload', function (data) {
+            res += '2';
+            if (res.length >= 3) {
+                s.done();
+            }
+        });
+    }
+
+    function filter_step(s) {
+        s.on('upload', function(data, flags) {
+            s.send(data);
+            res += '3';
+        });
+
+        s.on('download', function(data, flags) {
+
+            if (!flags.last) {
+                res += '4';
+                s.send(data);
+
+            } else {
+                res += '5';
+                s.send(res, {last:1});
+                s.off('download');
+            }
+        });
+    }
+
+    function access_undecided(s) {
+        s.decline();
+    }
+
+    function access_allow(s) {
+        if (s.remoteAddress.match('127.0.0.1')) {
+            s.done();
+            return;
+        }
+
+        s.deny();
+    }
+
+    function access_deny(s) {
+        if (s.remoteAddress.match('127.0.0.1')) {
+            s.deny();
+            return;
+        }
+
+        s.allow();
+    }
+
+
+    function preread_async(s) {
+        setTimeout(function() {
+            s.done();
+        }, 1);
+    }
+
+    function preread_data(s) {
+        s.on('upload', function (data, flags) {
+            if (data.indexOf('z') != -1) {
+                s.done();
+            }
+        });
+    }
+
+    var line = '';
+
+    function preread_req_line(s) {
+        s.on('upload', function (data, flags) {
+            var n = data.indexOf('\\n');
+            if (n != -1) {
+                line = data.substr(0, n);
+                s.done();
+            }
+        });
+    }
+
+    function req_line(s) {
+        return line;
+    }
+
+    function filter_empty(s) {
+    }
+
+    function filter_header_inject(s) {
+        var req = '';
+
+        s.on('upload', function(data, flags) {
+            req += data;
+
+            var n = req.search('\\n');
+            if (n != -1) {
+                var rest = req.substr(n + 1);
+                req = req.substr(0, n + 1);
+
+                s.send(req + 'Foo: foo' + '\\r\\n' + rest, flags);
+
+                s.off('upload');
+            }
+        });
+    }
+
+    function filter_search(s) {
+        s.on('download', function(data, flags) {
+            var n = data.search('y');
+            if (n != -1) {
+                s.send('z');
+            }
+        });
+
+        s.on('upload', function(data, flags) {
+            var n = data.search('x');
+            if (n != -1) {
+                s.send('y');
+            }
+        });
+    }
+
+    function access_except(s) {
+        function done() {return s.a.a};
+
+        setTimeout(done, 1);
+        setTimeout(done, 2);
+    }
+
+    function preread_except(s) {
+        var fs = require('fs');
+        fs.readFileSync();
+    }
+
+    function filter_except(s) {
+        s.on('unknown', function() {});
+    }
+
+    function pr(x) {
+        return new Promise(resolve => {resolve(x)}).then(v => v).then(v => v);
+    }
+
+    async function asyncf(s) {
+        const a1 = await pr(10);
+        const a2 = await pr(20);
+
+        s.setReturnValue(`retval: \${a1 + a2}`);
+    }
+
+    export default {njs:test_njs, addr, variable, sess_unk, log, access_step,
+                    preread_step, filter_step, access_undecided, access_allow,
+                    access_deny, preread_async, preread_data, preread_req_line,
+                    req_line, filter_empty, filter_header_inject, filter_search,
+                    access_except, preread_except, filter_except, asyncf};
+
+EOF
+
+$t->run_daemon(\&stream_daemon, port(8090));
+$t->try_run('no stream njs available')->plan(23);
+$t->waitforsocket('127.0.0.1:' . port(8090));
+
+###############################################################################
+
+is(stream('127.0.0.1:' . port(8080))->read(), 'addr=127.0.0.1',
+       's.remoteAddress');
+is(dgram('127.0.0.1:' . port(8985))->io('.'), 'addr=127.0.0.1',
+       's.remoteAddress udp');
+is(stream('127.0.0.1:' . port(8081))->read(), 'undefined', 's.log');
+is(stream('127.0.0.1:' . port(8082))->read(), 'variable=127.0.0.1',
+       's.variables');
+is(stream('127.0.0.1:' . port(8083))->read(), '', 'stream js unknown function');
+is(stream('127.0.0.1:' . port(8084))->read(), 'sess_unk=undefined', 's.unk');
+
+is(stream('127.0.0.1:' . port(8086))->io('0'), '0122345',
+       'async handlers order');
+is(stream('127.0.0.1:' . port(8087))->io('#'), 'OK', 'access_undecided');
+is(stream('127.0.0.1:' . port(8088))->io('#'), 'OK', 'access_allow');
+is(stream('127.0.0.1:' . port(8089))->io('#'), '', 'access_deny');
+
+is(stream('127.0.0.1:' . port(8091))->io('#'), '#', 'preread_async');
+is(stream('127.0.0.1:' . port(8092))->io('#z'), '#z', 'preread_async_data');
+is(stream('127.0.0.1:' . port(8093))->io("xy\na"), 'xy', 'preread_req_line');
+
+is(stream('127.0.0.1:' . port(8094))->io('x'), 'x', 'filter_empty');
+like(get('/p/return'), qr/foo/, 'filter_injected_header');
+is(stream('127.0.0.1:' . port(8096))->io('x'), 'z', 'filter_search');
+
+stream('127.0.0.1:' . port(8097))->io('x');
+stream('127.0.0.1:' . port(8098))->io('x');
+stream('127.0.0.1:' . port(8099))->io('x');
+
+is(stream('127.0.0.1:' . port(8100))->read(), 'retval: 30', 'asyncf');
+
+$t->stop();
+
+ok(index($t->read_file('error.log'), 'SEE-THIS') > 0, 'stream js log');
+ok(index($t->read_file('error.log'), 'at fs.readFileSync') > 0,
+       'stream js_preread backtrace');
+ok(index($t->read_file('error.log'), 'at filter_except') > 0,
+       'stream js_filter backtrace');
+
+my @p = (port(8087), port(8088), port(8089));
+like($t->read_file('status.log'), qr/$p[0]:200/, 'status undecided');
+like($t->read_file('status.log'), qr/$p[1]:200/, 'status allow');
+like($t->read_file('status.log'), qr/$p[2]:403/, 'status deny');
+
+###############################################################################
+
+sub stream_daemon {
+       my $server = IO::Socket::INET->new(
+               Proto => 'tcp',
+               LocalAddr => '127.0.0.1:' . port(8090),
+               Listen => 5,
+               Reuse => 1
+       )
+               or die "Can't create listening socket: $!\n";
+
+       local $SIG{PIPE} = 'IGNORE';
+
+       while (my $client = $server->accept()) {
+               $client->autoflush(1);
+
+               log2c("(new connection $client)");
+
+               $client->sysread(my $buffer, 65536) or next;
+
+               log2i("$client $buffer");
+
+               log2o("$client $buffer");
+
+               $client->syswrite($buffer);
+
+               close $client;
+       }
+}
+
+sub log2i { Test::Nginx::log_core('|| <<', @_); }
+sub log2o { Test::Nginx::log_core('|| >>', @_); }
+sub log2c { Test::Nginx::log_core('||', @_); }
+
+sub get {
+       my ($url, %extra) = @_;
+
+       my $s = IO::Socket::INET->new(
+               Proto => 'tcp',
+               PeerAddr => '127.0.0.1:' . port(8079)
+       ) or die "Can't connect to nginx: $!\n";
+
+       return http_get($url, socket => $s);
+}
+
+###############################################################################
diff --git a/nginx/t/stream_js_buffer.t b/nginx/t/stream_js_buffer.t
new file mode 100644 (file)
index 0000000..cc96013
--- /dev/null
@@ -0,0 +1,177 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for stream njs module, buffer properties.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http proxy rewrite stream stream_return/)
+       ->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 /p/ {
+            proxy_pass http://127.0.0.1:8085/;
+        }
+
+        location /return {
+            return 200 'RETURN:$http_foo';
+        }
+    }
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_import test.js;
+
+    js_set $type        test.type;
+    js_set $binary_var  test.binary_var;
+
+    server {
+        listen  127.0.0.1:8081;
+        return  $type;
+    }
+
+    server {
+        listen  127.0.0.1:8082;
+        return  $binary_var;
+    }
+
+    server {
+        listen      127.0.0.1:8083;
+        js_preread  test.cb_mismatch;
+        proxy_pass  127.0.0.1:8090;
+    }
+
+    server {
+        listen      127.0.0.1:8084;
+        js_preread  test.cb_mismatch2;
+        proxy_pass  127.0.0.1:8090;
+    }
+
+    server {
+        listen      127.0.0.1:8085;
+        js_filter   test.header_inject;
+        proxy_pass  127.0.0.1:8080;
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function type(s) {
+               var v = s.rawVariables.remote_addr;
+               var type = Buffer.isBuffer(v) ? 'buffer' : (typeof v);
+               return type;
+    }
+
+    function binary_var(s) {
+        var test = s.rawVariables
+                   .binary_remote_addr.equals(Buffer.from([127,0,0,1]));
+        return test;
+    }
+
+    function cb_mismatch(s) {
+        try {
+            s.on('upload', () => {});
+            s.on('downstream', () => {});
+        } catch (e) {
+            throw new Error(`cb_mismatch:\${e.message}`)
+        }
+    }
+
+    function cb_mismatch2(s) {
+        try {
+            s.on('upstream', () => {});
+            s.on('download', () => {});
+        } catch (e) {
+            throw new Error(`cb_mismatch2:\${e.message}`)
+        }
+    }
+
+    function header_inject(s) {
+        var req = Buffer.from([]);
+
+        s.on('upstream', function(data, flags) {
+            req = Buffer.concat([req, data]);
+
+            var n = req.indexOf('\\n');
+            if (n != -1) {
+                var rest = req.slice(n + 1);
+                req = req.slice(0, n + 1);
+
+                s.send(req, flags);
+                s.send('Foo: foo\\r\\n', flags);
+                s.send(rest, flags);
+
+                s.off('upstream');
+            }
+        });
+    }
+
+    export default {njs: test_njs, type, binary_var, cb_mismatch, cb_mismatch2,
+                    header_inject};
+
+EOF
+
+$t->try_run('no njs ngx')->plan(5);
+
+###############################################################################
+
+is(stream('127.0.0.1:' . port(8081))->read(), 'buffer', 'var type');
+is(stream('127.0.0.1:' . port(8082))->read(), 'true', 'binary var');
+
+stream('127.0.0.1:' . port(8083))->io('x');
+stream('127.0.0.1:' . port(8084))->io('x');
+
+like(http_get('/p/return'), qr/RETURN:foo/, 'injected header');
+
+$t->stop();
+
+ok(index($t->read_file('error.log'), 'cb_mismatch:mixing string and buffer')
+   > 0, 'cb mismatch');
+ok(index($t->read_file('error.log'), 'cb_mismatch2:mixing string and buffer')
+   > 0, 'cb mismatch');
+
+###############################################################################
diff --git a/nginx/t/stream_js_exit.t b/nginx/t/stream_js_exit.t
new file mode 100644 (file)
index 0000000..a8bc34a
--- /dev/null
@@ -0,0 +1,152 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for stream njs module, exit hook.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http stream/)
+       ->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;
+        }
+    }
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_import test.js;
+
+    server {
+        listen      127.0.0.1:8081;
+        js_access   test.access;
+        js_filter   test.filter;
+        proxy_pass  127.0.0.1:8090;
+    }
+
+    server {
+        listen      127.0.0.1:8082;
+        js_access   test.access;
+        proxy_pass  127.0.0.1:1;
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function access(s) {
+        njs.on('exit', () => {
+            var v = s.variables;
+            var c = `\${v.bytes_received}/\${v.bytes_sent}`;
+            var u = `\${v.upstream_bytes_received}/\${v.upstream_bytes_sent}`;
+            s.error(`s:\${s.status} C: \${c} U: \${u}`);
+        });
+
+        s.allow();
+    }
+
+    function filter(s) {
+        s.on('upload', (data, flags) => {
+            s.send(`@\${data}`, flags);
+        });
+
+        s.on('download', (data, flags) => {
+            s.send(data.slice(2), flags);
+        });
+    }
+
+    export default {njs: test_njs, access, filter};
+EOF
+
+$t->try_run('no stream njs available')->plan(2);
+
+$t->run_daemon(\&stream_daemon, port(8090));
+$t->waitforsocket('127.0.0.1:' . port(8090));
+
+###############################################################################
+
+stream('127.0.0.1:' . port(8081))->io('###');
+stream('127.0.0.1:' . port(8082))->io('###');
+
+$t->stop();
+
+ok(index($t->read_file('error.log'), 's:200 C: 3/6 U: 8/4') > 0, 'normal');
+ok(index($t->read_file('error.log'), 's:502 C: 0/0 U: 0/0') > 0, 'failed conn');
+
+###############################################################################
+
+sub stream_daemon {
+       my $server = IO::Socket::INET->new(
+               Proto => 'tcp',
+               LocalAddr => '127.0.0.1:' . port(8090),
+               Listen => 5,
+               Reuse => 1
+       )
+               or die "Can't create listening socket: $!\n";
+
+       local $SIG{PIPE} = 'IGNORE';
+
+       while (my $client = $server->accept()) {
+               $client->autoflush(1);
+
+               log2c("(new connection $client)");
+
+               $client->sysread(my $buffer, 65536) or next;
+
+               log2i("$client $buffer");
+
+               $buffer = $buffer . $buffer;
+
+               log2o("$client $buffer");
+
+               $client->syswrite($buffer);
+
+               close $client;
+       }
+}
+
+sub log2i { Test::Nginx::log_core('|| <<', @_); }
+sub log2o { Test::Nginx::log_core('|| >>', @_); }
+sub log2c { Test::Nginx::log_core('||', @_); }
+
+###############################################################################
diff --git a/nginx/t/stream_js_fetch.t b/nginx/t/stream_js_fetch.t
new file mode 100644 (file)
index 0000000..106702d
--- /dev/null
@@ -0,0 +1,277 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for stream njs module, fetch method.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http stream/)
+       ->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 /validate {
+            js_content test.validate;
+        }
+
+        location /success {
+            return 200;
+        }
+
+        location /fail {
+            return 403;
+        }
+    }
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_import test.js;
+
+    server {
+        listen      127.0.0.1:8081;
+        js_preread  test.preread_verify;
+        proxy_pass  127.0.0.1:8090;
+    }
+
+    server {
+        listen      127.0.0.1:8082;
+        js_filter   test.filter_verify;
+        proxy_pass  127.0.0.1:8091;
+    }
+
+    server {
+        listen      127.0.0.1:8083;
+        js_access   test.access_ok;
+        proxy_pass  127.0.0.1:8090;
+    }
+
+    server {
+        listen      127.0.0.1:8084;
+        js_access   test.access_nok;
+        proxy_pass  127.0.0.1:8090;
+    }
+}
+
+EOF
+
+my $p = port(8080);
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function validate(r) {
+        r.return((r.requestText == 'QZ') ? 200 : 403);
+    }
+
+    function preread_verify(s) {
+        var collect = Buffer.from([]);
+
+        s.on('upstream', async function (data, flags) {
+            collect = Buffer.concat([collect, data]);
+
+            if (collect.length >= 4 && collect.readUInt16BE(0) == 0xabcd) {
+                s.off('upstream');
+
+                let reply = await ngx.fetch('http://127.0.0.1:$p/validate',
+                                            {body: collect.slice(2,4)});
+
+                (reply.status == 200) ? s.done(): s.deny();
+
+            } else if (collect.length) {
+                s.deny();
+            }
+        });
+    }
+
+    function filter_verify(s) {
+        var collect = Buffer.from([]);
+
+        s.on('upstream', async function (data, flags) {
+            collect = Buffer.concat([collect, data]);
+
+            if (collect.length >= 4 && collect.readUInt16BE(0) == 0xabcd) {
+                s.off('upstream');
+
+                let reply = await ngx.fetch('http://127.0.0.1:$p/validate',
+                                            {body: collect.slice(2,4)});
+
+                if (reply.status == 200) {
+                    s.send(collect.slice(4), flags);
+
+                } else {
+                    s.send("__CLOSE__", flags);
+                }
+            }
+        });
+    }
+
+    async function access_ok(s) {
+        let reply = await ngx.fetch('http://127.0.0.1:$p/success');
+
+        (reply.status == 200) ? s.allow(): s.deny();
+    }
+
+    async function access_nok(s) {
+        let reply = await ngx.fetch('http://127.0.0.1:$p/fail');
+
+        (reply.status == 200) ? s.allow(): s.deny();
+    }
+
+    export default {njs: test_njs, validate, preread_verify, filter_verify,
+                    access_ok, access_nok};
+EOF
+
+$t->try_run('no stream njs available')->plan(9);
+
+$t->run_daemon(\&stream_daemon, port(8090), port(8091));
+$t->waitforsocket('127.0.0.1:' . port(8090));
+$t->waitforsocket('127.0.0.1:' . port(8091));
+
+###############################################################################
+
+is(stream('127.0.0.1:' . port(8081))->io('###'), '', 'preread not enough');
+is(stream('127.0.0.1:' . port(8081))->io("\xAB\xCDQZ##"), "\xAB\xCDQZ##",
+       'preread validated');
+is(stream('127.0.0.1:' . port(8081))->io("\xAC\xCDQZ##"), '',
+       'preread invalid magic');
+is(stream('127.0.0.1:' . port(8081))->io("\xAB\xCDQQ##"), '',
+       'preread validation failed');
+
+TODO: {
+todo_skip 'leaves coredump', 3 unless $ENV{TEST_NGINX_UNSAFE}
+       or has_version('0.7.7');
+
+my $s = stream('127.0.0.1:' . port(8082));
+is($s->io("\xAB\xCDQZ##", read => 1), '##', 'filter validated');
+is($s->io("@@", read => 1), '@@', 'filter off');
+
+is(stream('127.0.0.1:' . port(8082))->io("\xAB\xCDQQ##"), '',
+       'filter validation failed');
+
+}
+
+is(stream('127.0.0.1:' . port(8083))->io('ABC'), 'ABC', 'access fetch ok');
+is(stream('127.0.0.1:' . port(8084))->io('ABC'), '', 'access fetch nok');
+
+###############################################################################
+
+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;
+}
+
+###############################################################################
+
+sub stream_daemon {
+       my (@ports) = @_;
+       my (@socks, @clients);
+
+       for my $port (@ports) {
+               my $server = IO::Socket::INET->new(
+                       Proto => 'tcp',
+                       LocalAddr => "127.0.0.1:$port",
+                       Listen => 5,
+                       Reuse => 1
+               )
+                       or die "Can't create listening socket: $!\n";
+               push @socks, $server;
+       }
+
+       my $sel = IO::Select->new(@socks);
+
+       local $SIG{PIPE} = 'IGNORE';
+
+       while (my @ready = $sel->can_read) {
+               foreach my $fh (@ready) {
+                       if (grep $_ == $fh, @socks) {
+                               my $new = $fh->accept;
+                               $new->autoflush(1);
+                               $sel->add($new);
+
+                       } elsif (stream_handle_client($fh)
+                               || $fh->sockport() == port(8090))
+                       {
+                               $sel->remove($fh);
+                               $fh->close;
+                       }
+               }
+       }
+}
+
+sub stream_handle_client {
+       my ($client) = @_;
+
+       log2c("(new connection $client)");
+
+       $client->sysread(my $buffer, 65536) or return 1;
+
+       log2i("$client $buffer");
+
+       if ($buffer eq "__CLOSE__") {
+               return 1;
+       }
+
+       log2o("$client $buffer");
+
+       $client->syswrite($buffer);
+
+       return 0;
+}
+
+sub log2i { Test::Nginx::log_core('|| <<', @_); }
+sub log2o { Test::Nginx::log_core('|| >>', @_); }
+sub log2c { Test::Nginx::log_core('||', @_); }
+
+###############################################################################
diff --git a/nginx/t/stream_js_fetch_https.t b/nginx/t/stream_js_fetch_https.t
new file mode 100644 (file)
index 0000000..056352f
--- /dev/null
@@ -0,0 +1,404 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for stream njs module, fetch method, https support.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()
+       ->has(qw/http http_ssl rewrite stream stream_return socket_ssl/)
+       ->has_daemon('openssl')
+       ->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;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8081 ssl default;
+        server_name  default.example.com;
+
+        ssl_certificate default.example.com.chained.crt;
+        ssl_certificate_key default.example.com.key;
+
+        location /loc {
+            return 200 "You are at default.example.com.";
+        }
+
+        location /success {
+            return 200;
+        }
+
+        location /fail {
+            return 403;
+        }
+
+        location /backend {
+            return 200 "BACKEND OK";
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8081 ssl;
+        server_name  1.example.com;
+
+        ssl_certificate 1.example.com.chained.crt;
+        ssl_certificate_key 1.example.com.key;
+
+        location /loc {
+            return 200 "You are at 1.example.com.";
+        }
+    }
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_import   test.js;
+    js_var      $message;
+
+    resolver  127.0.0.1:%%PORT_8981_UDP%%;
+    resolver_timeout 1s;
+
+    server {
+        listen  127.0.0.1:8082;
+        js_preread  test.preread;
+        return  "default CA $message";
+    }
+
+    server {
+        listen  127.0.0.1:8083;
+        js_preread  test.preread;
+        return  "my CA $message";
+
+        js_fetch_ciphers HIGH:!aNull:!MD5;
+        js_fetch_protocols TLSv1.1 TLSv1.2;
+        js_fetch_trusted_certificate myca.crt;
+    }
+
+    server {
+        listen  127.0.0.1:8084;
+        js_preread  test.preread;
+        return  "my CA with verify_depth=0 $message";
+
+        js_fetch_verify_depth 0;
+        js_fetch_trusted_certificate myca.crt;
+    }
+
+    server {
+        listen  127.0.0.1:8085;
+
+        js_access test.access_ok;
+        ssl_preread on;
+
+        js_fetch_ciphers HIGH:!aNull:!MD5;
+        js_fetch_protocols TLSv1.1 TLSv1.2;
+        js_fetch_trusted_certificate myca.crt;
+
+        proxy_pass 127.0.0.1:8081;
+    }
+
+    server {
+        listen  127.0.0.1:8086;
+
+        js_access test.access_nok;
+        ssl_preread on;
+
+        js_fetch_ciphers HIGH:!aNull:!MD5;
+        js_fetch_protocols TLSv1.1 TLSv1.2;
+        js_fetch_trusted_certificate myca.crt;
+
+        proxy_pass 127.0.0.1:8081;
+    }
+}
+
+EOF
+
+my $p1 = port(8081);
+my $p2 = port(8082);
+my $p3 = port(8083);
+my $p4 = port(8084);
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function preread(s) {
+        s.on('upload', function (data, flags) {
+            if (data.startsWith('GO')) {
+                s.off('upload');
+                ngx.fetch('https://' + data.substring(2) + ':$p1/loc')
+                   .then(reply => {
+                       s.variables.message = 'https OK - ' + reply.status;
+                       s.done();
+                   })
+                   .catch(e => {
+                       s.variables.message = 'https NOK - ' + e.message;
+                       s.done();
+                   })
+
+            } else if (data.length) {
+                s.deny();
+            }
+        });
+    }
+
+    async function access_ok(s) {
+        let r = await ngx.fetch('https://default.example.com:$p1/success',
+                                {body: s.remoteAddress});
+
+        (r.status == 200) ? s.allow(): s.deny();
+    }
+
+    async function access_nok(s) {
+        let r = await ngx.fetch('https://default.example.com:$p1/fail',
+                                {body: s.remoteAddress});
+
+        (r.status == 200) ? s.allow(): s.deny();
+    }
+
+    export default {njs: test_njs, preread, access_ok, access_nok};
+EOF
+
+my $d = $t->testdir();
+
+$t->write_file('openssl.conf', <<EOF);
+[ req ]
+default_bits = 2048
+encrypt_key = no
+distinguished_name = req_distinguished_name
+[ req_distinguished_name ]
+EOF
+
+$t->write_file('myca.conf', <<EOF);
+[ ca ]
+default_ca = myca
+
+[ myca ]
+new_certs_dir = $d
+database = $d/certindex
+default_md = sha256
+policy = myca_policy
+serial = $d/certserial
+default_days = 1
+x509_extensions = myca_extensions
+
+[ myca_policy ]
+commonName = supplied
+
+[ myca_extensions ]
+basicConstraints = critical,CA:TRUE
+EOF
+
+system('openssl req -x509 -new '
+       . "-config $d/openssl.conf -subj /CN=myca/ "
+       . "-out $d/myca.crt -keyout $d/myca.key "
+       . ">>$d/openssl.out 2>&1") == 0
+       or die "Can't create self-signed certificate for CA: $!\n";
+
+foreach my $name ('intermediate', 'default.example.com', '1.example.com') {
+       system("openssl req -new "
+               . "-config $d/openssl.conf -subj /CN=$name/ "
+               . "-out $d/$name.csr -keyout $d/$name.key "
+               . ">>$d/openssl.out 2>&1") == 0
+               or die "Can't create certificate signing req for $name: $!\n";
+}
+
+$t->write_file('certserial', '1000');
+$t->write_file('certindex', '');
+
+system("openssl ca -batch -config $d/myca.conf "
+       . "-keyfile $d/myca.key -cert $d/myca.crt "
+       . "-subj /CN=intermediate/ -in $d/intermediate.csr "
+       . "-out $d/intermediate.crt "
+       . ">>$d/openssl.out 2>&1") == 0
+       or die "Can't sign certificate for intermediate: $!\n";
+
+foreach my $name ('default.example.com', '1.example.com') {
+       system("openssl ca -batch -config $d/myca.conf "
+               . "-keyfile $d/intermediate.key -cert $d/intermediate.crt "
+               . "-subj /CN=$name/ -in $d/$name.csr -out $d/$name.crt "
+               . ">>$d/openssl.out 2>&1") == 0
+               or die "Can't sign certificate for $name $!\n";
+       $t->write_file("$name.chained.crt", $t->read_file("$name.crt")
+               . $t->read_file('intermediate.crt'));
+}
+
+$t->try_run('no njs.fetch')->plan(6);
+
+$t->run_daemon(\&dns_daemon, port(8981), $t);
+$t->waitforfile($t->testdir . '/' . port(8981));
+
+###############################################################################
+
+like(stream("127.0.0.1:$p2")->io('GOdefault.example.com'),
+       qr/connect failed/s, 'stream non trusted CA');
+like(stream("127.0.0.1:$p3")->io('GOdefault.example.com'),
+       qr/https OK/s, 'stream trusted CA');
+like(stream("127.0.0.1:$p3")->io('GOlocalhost'),
+       qr/connect failed/s, 'stream wrong CN');
+like(stream("127.0.0.1:$p4")->io('GOdefaul.example.com'),
+       qr/connect failed/s, 'stream verify_depth too small');
+
+like(https_get('default.example.com', port(8085), '/backend'),
+       qr!BACKEND OK!, 'access https fetch');
+is(https_get('default.example.com', port(8086), '/backend'), '<conn failed>',
+       'access https fetch not');
+
+###############################################################################
+
+sub get_ssl_socket {
+       my ($host, $port) = @_;
+       my $s;
+
+       eval {
+               local $SIG{ALRM} = sub { die "timeout\n" };
+               local $SIG{PIPE} = sub { die "sigpipe\n" };
+               alarm(8);
+               $s = IO::Socket::SSL->new(
+                       Proto => 'tcp',
+                       PeerAddr => '127.0.0.1:' . $port,
+                       SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(),
+                       SSL_error_trap => sub { die $_[1] }
+               );
+
+               alarm(0);
+       };
+
+       alarm(0);
+
+       if ($@) {
+               log_in("died: $@");
+               return undef;
+       }
+
+       return $s;
+}
+
+sub https_get {
+       my ($host, $port, $url) = @_;
+       my $s = get_ssl_socket($host, $port);
+
+       if (!$s) {
+               return '<conn failed>';
+       }
+
+       return http(<<EOF, socket => $s);
+GET $url HTTP/1.0
+Host: $host
+
+EOF
+}
+
+###############################################################################
+
+sub reply_handler {
+       my ($recv_data, $port, %extra) = @_;
+
+       my (@name, @rdata);
+
+       use constant NOERROR    => 0;
+       use constant A          => 1;
+       use constant IN         => 1;
+
+       # default values
+
+       my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 3600);
+
+       # decode name
+
+       my ($len, $offset) = (undef, 12);
+       while (1) {
+               $len = unpack("\@$offset C", $recv_data);
+               last if $len == 0;
+               $offset++;
+               push @name, unpack("\@$offset A$len", $recv_data);
+               $offset += $len;
+       }
+
+       $offset -= 1;
+       my ($id, $type, $class) = unpack("n x$offset n2", $recv_data);
+
+       my $name = join('.', @name);
+
+       if ($type == A) {
+               push @rdata, rd_addr($ttl, '127.0.0.1');
+       }
+
+       $len = @name;
+       pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata,
+               0, 0, @name, $type, $class) . join('', @rdata);
+}
+
+sub rd_addr {
+       my ($ttl, $addr) = @_;
+
+       my $code = 'split(/\./, $addr)';
+
+       return pack 'n3N', 0xc00c, A, IN, $ttl if $addr eq '';
+
+       pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code);
+}
+
+sub dns_daemon {
+       my ($port, $t) = @_;
+
+       my ($data, $recv_data);
+       my $socket = IO::Socket::INET->new(
+               LocalAddr    => '127.0.0.1',
+               LocalPort    => $port,
+               Proto        => 'udp',
+       )
+               or die "Can't create listening socket: $!\n";
+
+       local $SIG{PIPE} = 'IGNORE';
+
+       # signal we are ready
+
+       open my $fh, '>', $t->testdir() . '/' . $port;
+       close $fh;
+
+       while (1) {
+               $socket->recv($recv_data, 65536);
+               $data = reply_handler($recv_data, $port);
+               $socket->send($data);
+       }
+}
+
+###############################################################################
diff --git a/nginx/t/stream_js_fetch_init.t b/nginx/t/stream_js_fetch_init.t
new file mode 100644 (file)
index 0000000..6de487d
--- /dev/null
@@ -0,0 +1,149 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for stream njs module, Response prototype reinitialization.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http rewrite stream/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_import test.js;
+
+    server {
+        listen      127.0.0.1:8081;
+        js_access   test.access_ok;
+        proxy_pass  127.0.0.1:8090;
+    }
+}
+
+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 /success {
+            return 200;
+        }
+    }
+}
+
+EOF
+
+my $p = port(8080);
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    async function access_ok(s) {
+        let reply = await ngx.fetch('http://127.0.0.1:$p/success');
+
+        (reply.status == 200) ? s.allow(): s.deny();
+    }
+
+    export default {njs: test_njs, access_ok};
+EOF
+
+$t->try_run('no stream njs available')->plan(1);
+
+$t->run_daemon(\&stream_daemon, port(8090));
+$t->waitforsocket('127.0.0.1:' . port(8090));
+
+###############################################################################
+
+local $TODO = 'not yet' unless has_version('0.7.9');
+
+is(stream('127.0.0.1:' . port(8081))->io('ABC'), 'ABC', 'access fetch ok');
+
+###############################################################################
+
+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;
+}
+
+###############################################################################
+
+sub stream_daemon {
+       my $server = IO::Socket::INET->new(
+               Proto => 'tcp',
+               LocalAddr => '127.0.0.1:' . port(8090),
+               Listen => 5,
+               Reuse => 1
+       )
+               or die "Can't create listening socket: $!\n";
+
+       local $SIG{PIPE} = 'IGNORE';
+
+       while (my $client = $server->accept()) {
+               $client->autoflush(1);
+
+               log2c("(new connection $client)");
+
+               $client->sysread(my $buffer, 65536) or next;
+
+               log2i("$client $buffer");
+
+               log2o("$client $buffer");
+
+               $client->syswrite($buffer);
+
+               close $client;
+       }
+}
+
+sub log2i { Test::Nginx::log_core('|| <<', @_); }
+sub log2o { Test::Nginx::log_core('|| >>', @_); }
+sub log2c { Test::Nginx::log_core('||', @_); }
+
+###############################################################################
diff --git a/nginx/t/stream_js_import.t b/nginx/t/stream_js_import.t
new file mode 100644 (file)
index 0000000..7000060
--- /dev/null
@@ -0,0 +1,117 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for stream njs module, js_import directive.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http stream stream_return/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_set $test foo.bar.p;
+
+    js_import lib.js;
+    js_import foo from ./main.js;
+
+    server {
+        listen  127.0.0.1:8081;
+        return  $test;
+    }
+
+    server {
+        listen      127.0.0.1:8082;
+        js_access   lib.access;
+        js_preread  lib.preread;
+        js_filter   lib.filter;
+        proxy_pass  127.0.0.1:8083;
+    }
+
+    server {
+        listen  127.0.0.1:8083;
+        return  "x";
+    }
+}
+
+EOF
+
+$t->write_file('lib.js', <<EOF);
+    var res = '';
+
+    function access(s) {
+        res += '1';
+        s.allow();
+    }
+
+    function preread(s) {
+        s.on('upload', function (data) {
+            res += '2';
+            if (res.length >= 3) {
+                s.done();
+            }
+        });
+    }
+
+    function filter(s) {
+        s.on('upload', function(data, flags) {
+            s.send(data);
+            res += '3';
+        });
+
+        s.on('download', function(data, flags) {
+            if (!flags.last) {
+                res += '4';
+                s.send(data);
+
+            } else {
+                res += '5';
+                s.send(res, {last:1});
+                s.off('download');
+            }
+        });
+    }
+
+    export default {access, preread, filter};
+
+EOF
+
+$t->write_file('main.js', <<EOF);
+    export default {bar: {p(s) {return "P-TEST"}}};
+
+EOF
+
+$t->try_run('no njs available')->plan(2);
+
+###############################################################################
+
+is(stream('127.0.0.1:' . port(8081))->read(), 'P-TEST', 'foo.bar.p');
+is(stream('127.0.0.1:' . port(8082))->io('0'), 'x122345', 'lib.access');
+
+###############################################################################
diff --git a/nginx/t/stream_js_import2.t b/nginx/t/stream_js_import2.t
new file mode 100644 (file)
index 0000000..a057c25
--- /dev/null
@@ -0,0 +1,117 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for stream njs module, js_import directive in server context.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/stream stream_return/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    server {
+        listen  127.0.0.1:8081;
+        js_import foo from ./main.js;
+        js_set $test foo.bar.p;
+        return  $test;
+    }
+
+    server {
+        listen      127.0.0.1:8082;
+
+        js_import lib.js;
+
+        js_access   lib.access;
+        js_preread  lib.preread;
+        js_filter   lib.filter;
+        proxy_pass  127.0.0.1:8083;
+    }
+
+    server {
+        listen  127.0.0.1:8083;
+        return  "x";
+    }
+}
+
+EOF
+
+$t->write_file('lib.js', <<EOF);
+    var res = '';
+
+    function access(s) {
+        res += '1';
+        s.allow();
+    }
+
+    function preread(s) {
+        s.on('upload', function (data) {
+            res += '2';
+            if (res.length >= 3) {
+                s.done();
+            }
+        });
+    }
+
+    function filter(s) {
+        s.on('upload', function(data, flags) {
+            s.send(data);
+            res += '3';
+        });
+
+        s.on('download', function(data, flags) {
+            if (!flags.last) {
+                res += '4';
+                s.send(data);
+
+            } else {
+                res += '5';
+                s.send(res, {last:1});
+                s.off('download');
+            }
+        });
+    }
+
+    export default {access, preread, filter};
+
+EOF
+
+$t->write_file('main.js', <<EOF);
+    export default {bar: {p(s) {return "P-TEST"}}};
+
+EOF
+
+$t->try_run('no njs available')->plan(2);
+
+###############################################################################
+
+is(stream('127.0.0.1:' . port(8081))->read(), 'P-TEST', 'foo.bar.p');
+is(stream('127.0.0.1:' . port(8082))->io('0'), 'x122345', 'lib.access');
+
+###############################################################################
diff --git a/nginx/t/stream_js_ngx.t b/nginx/t/stream_js_ngx.t
new file mode 100644 (file)
index 0000000..de6dc58
--- /dev/null
@@ -0,0 +1,94 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for stream njs module, ngx object.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/stream stream_return/)
+       ->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;
+        }
+    }
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_import test.js;
+
+    js_set $log     test.log;
+
+    server {
+        listen  127.0.0.1:8081;
+        return  $log;
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function log(s) {
+        ngx.log(ngx.INFO, `ngx.log:FOO`);
+        ngx.log(ngx.WARN, `ngx.log:BAR`);
+        ngx.log(ngx.ERR, `ngx.log:BAZ`);
+        return 'OK';
+    }
+
+    export default {njs: test_njs, log};
+
+EOF
+
+$t->try_run('no njs ngx')->plan(4);
+
+###############################################################################
+
+is(stream('127.0.0.1:' . port(8081))->read(), 'OK', 'log var');
+
+$t->stop();
+
+like($t->read_file('error.log'), qr/\[info\].*ngx.log:FOO/, 'ngx.log info');
+like($t->read_file('error.log'), qr/\[warn\].*ngx.log:BAR/, 'ngx.log warn');
+like($t->read_file('error.log'), qr/\[error\].*ngx.log:BAZ/, 'ngx.log err');
+
+###############################################################################
diff --git a/nginx/t/stream_js_object.t b/nginx/t/stream_js_object.t
new file mode 100644 (file)
index 0000000..504b934
--- /dev/null
@@ -0,0 +1,98 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for stream njs module, stream session object.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/stream stream_return/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_set $test     test.test;
+
+    js_import test.js;
+
+    server {
+        listen  127.0.0.1:8081;
+        return  $test$status;
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function to_string(s) {
+        return s.toString() === '[object Stream Session]';
+    }
+
+    function define_prop(s) {
+        Object.defineProperty(s.variables, 'status', {value:400});
+        return s.variables.status == 400;
+    }
+
+    function in_operator(s) {
+        return ['status', 'unknown']
+               .map(v=>v in s.variables)
+               .toString() === 'true,false';
+    }
+
+    function redefine_proto(s) {
+        s[0] = 'a';
+        s[1] = 'b';
+        s.length = 2;
+        Object.setPrototypeOf(s, Array.prototype);
+        return s.join('|') === 'a|b';
+    }
+
+    function get_own_prop_descs(s) {
+        return Object.getOwnPropertyDescriptors(s)['on'].value === s.on;
+    }
+
+    function test(s) {
+        return [ to_string,
+                 define_prop,
+                 in_operator,
+                 redefine_proto,
+                 get_own_prop_descs,
+               ].every(v=>v(s));
+    }
+
+    export default {test};
+
+EOF
+
+$t->try_run('no njs stream session object')->plan(1);
+
+###############################################################################
+
+is(stream('127.0.0.1:' . port(8081))->read(), 'true400', 'var set');
+
+###############################################################################
diff --git a/nginx/t/stream_js_preload_object.t b/nginx/t/stream_js_preload_object.t
new file mode 100644 (file)
index 0000000..c28c401
--- /dev/null
@@ -0,0 +1,122 @@
+#!/usr/bin/perl
+
+# (C) Vadim Zhestikov
+# (C) Nginx, Inc.
+
+# Tests for stream njs module, js_preload_object directive.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/stream stream_return/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_preload_object g1 from g.json;
+
+    js_set $test foo.bar.p;
+
+    js_import lib.js;
+    js_import foo from ./main.js;
+
+    server {
+        listen  127.0.0.1:8081;
+        return  $test;
+    }
+
+    server {
+        listen      127.0.0.1:8082;
+        js_access   lib.access;
+        js_preread  lib.preread;
+        js_filter   lib.filter;
+        proxy_pass  127.0.0.1:8083;
+    }
+
+    server {
+        listen  127.0.0.1:8083;
+        return  "x";
+    }
+}
+
+EOF
+
+$t->write_file('lib.js', <<EOF);
+    var res = '';
+
+    function access(s) {
+        res += g1.a;
+        s.allow();
+    }
+
+    function preread(s) {
+        s.on('upload', function (data) {
+            res += g1.b[1];
+            if (res.length >= 3) {
+                s.done();
+            }
+        });
+    }
+
+    function filter(s) {
+        s.on('upload', function(data, flags) {
+            s.send(data);
+            res += g1.c.prop[0].a;
+        });
+
+        s.on('download', function(data, flags) {
+            if (!flags.last) {
+                res += g1.b[3];
+                s.send(data);
+
+            } else {
+                res += g1.b[4];
+                s.send(res, {last:1});
+                s.off('download');
+            }
+        });
+    }
+
+    export default {access, preread, filter};
+
+EOF
+
+$t->write_file('main.js', <<EOF);
+    export default {bar: {p(s) {return g1.b[2]}}};
+
+EOF
+
+$t->write_file('g.json',
+       '{"a":1, "b":[1,2,"element",4,5], "c":{"prop":[{"a":3}]}}');
+
+$t->try_run('no js_preload_object available')->plan(2);
+
+###############################################################################
+
+is(stream('127.0.0.1:' . port(8081))->read(), 'element', 'foo.bar.p');
+is(stream('127.0.0.1:' . port(8082))->io('0'), 'x122345', 'lib.access');
+
+###############################################################################
diff --git a/nginx/t/stream_js_send.t b/nginx/t/stream_js_send.t
new file mode 100644 (file)
index 0000000..318be6a
--- /dev/null
@@ -0,0 +1,186 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for s.send() in stream njs module.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http stream/)
+       ->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;
+        }
+    }
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_import test.js;
+
+    server {
+        listen      127.0.0.1:8081;
+        js_filter   test.filter;
+        proxy_pass  127.0.0.1:8090;
+    }
+
+    server {
+        listen      127.0.0.1:8082;
+        js_filter   test.filter_direct;
+        proxy_pass  127.0.0.1:8090;
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function test_njs(r) {
+        r.return(200, njs.version);
+    }
+
+    function filter(s) {
+      s.on("upload", async (data, flags) => {
+        s.send("__HANDSHAKE__", flags);
+
+        const p = new Promise((resolve, reject) => {
+              s.on("download", (data, flags) => {
+                  s.off("download");
+                  resolve(data);
+              });
+        });
+
+        s.off("upload");
+
+        const handshakeResponse = await p;
+        if (handshakeResponse != '__HANDSHAKE_RESPONSE__') {
+            throw `Handshake failed: \${handshakeResponse}`;
+        }
+
+        s.send(data, flags);
+      });
+    }
+
+    function filter_direct(s) {
+      s.on("upload", async (data, flags) => {
+        s.sendUpstream("__HANDSHAKE__", flags);
+
+        const p = new Promise((resolve, reject) => {
+              s.on("download", (data, flags) => {
+                  s.off("download");
+                  resolve(data);
+              });
+        });
+
+        s.off("upload");
+
+        const handshakeResponse = await p;
+        if (handshakeResponse != '__HANDSHAKE_RESPONSE__') {
+            throw `Handshake failed: \${handshakeResponse}`;
+        }
+
+        s.sendDownstream('xxx', flags);
+        s.sendUpstream(data, flags);
+      });
+    }
+
+    export default {njs:test_njs, filter, filter_direct};
+
+EOF
+
+$t->run_daemon(\&stream_daemon, port(8090));
+$t->try_run('no stream njs available')->plan(2);
+$t->waitforsocket('127.0.0.1:' . port(8090));
+
+###############################################################################
+
+is(stream('127.0.0.1:' . port(8081))->io('abc'), 'ABC',
+       'async filter');;
+is(stream('127.0.0.1:' . port(8082))->io('abc'), 'xxxABC',
+       'async filter direct');
+
+$t->stop();
+
+###############################################################################
+
+sub stream_daemon {
+       my $server = IO::Socket::INET->new(
+               Proto => 'tcp',
+               LocalAddr => '127.0.0.1:' . port(8090),
+               Listen => 5,
+               Reuse => 1
+       )
+               or die "Can't create listening socket: $!\n";
+
+       local $SIG{PIPE} = 'IGNORE';
+
+       while (my $client = $server->accept()) {
+               $client->autoflush(1);
+
+               log2c("(new connection $client)");
+
+               $client->sysread(my $buffer, 65536) or next;
+
+               log2i("$client $buffer");
+
+               if ($buffer ne "__HANDSHAKE__") {
+                       $buffer = "__HANDSHAKE_INVALID__";
+                       log2o("$client $buffer");
+                       $client->syswrite($buffer);
+
+                       close $client;
+               }
+
+               $buffer = "__HANDSHAKE_RESPONSE__";
+               log2o("$client $buffer");
+               $client->syswrite($buffer);
+
+               $client->sysread($buffer, 65536) or next;
+
+               $buffer = uc($buffer);
+               log2o("$client $buffer");
+               $client->syswrite($buffer);
+
+               close $client;
+       }
+}
+
+sub log2i { Test::Nginx::log_core('|| <<', @_); }
+sub log2o { Test::Nginx::log_core('|| >>', @_); }
+sub log2c { Test::Nginx::log_core('||', @_); }
+
+###############################################################################
diff --git a/nginx/t/stream_js_var.t b/nginx/t/stream_js_var.t
new file mode 100644 (file)
index 0000000..cde2dc9
--- /dev/null
@@ -0,0 +1,75 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for stream njs module, js_var directive.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/stream stream_return/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_import test.js;
+
+    js_var $foo;
+    js_var $bar a:$remote_addr;
+    js_set $var test.varr;
+
+    server {
+        listen  127.0.0.1:8081;
+        return  $bar$foo;
+    }
+
+    server {
+        listen  127.0.0.1:8082;
+        return  $var$foo;
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function varr(s) {
+        s.variables.foo = 'xxx';
+        return '';
+    }
+
+    export default {varr};
+EOF
+
+$t->try_run('no stream js_var')->plan(2);
+
+###############################################################################
+
+is(stream('127.0.0.1:' . port(8081))->io('###'), 'a:127.0.0.1',
+       'default value');
+is(stream('127.0.0.1:' . port(8082))->io('###'), 'xxx', 'value set');
+
+###############################################################################
diff --git a/nginx/t/stream_js_var2.t b/nginx/t/stream_js_var2.t
new file mode 100644 (file)
index 0000000..71cee1e
--- /dev/null
@@ -0,0 +1,75 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for stream njs module, js_var directive in server context.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/stream stream_return/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_import test.js;
+
+    js_var $foo;
+
+    server {
+        listen  127.0.0.1:8081;
+        js_var $bar a:$remote_addr;
+        return  $bar$foo;
+    }
+
+    server {
+        listen  127.0.0.1:8082;
+        js_set $var test.varr;
+        return  $var$foo;
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function varr(s) {
+        s.variables.foo = 'xxx';
+        return '';
+    }
+
+    export default {varr};
+EOF
+
+$t->try_run('no stream js_var')->plan(2);
+
+###############################################################################
+
+is(stream('127.0.0.1:' . port(8081))->io('###'), 'a:127.0.0.1',
+       'default value');
+is(stream('127.0.0.1:' . port(8082))->io('###'), 'xxx', 'value set');
+
+###############################################################################
diff --git a/nginx/t/stream_js_variables.t b/nginx/t/stream_js_variables.t
new file mode 100644 (file)
index 0000000..29e6c33
--- /dev/null
@@ -0,0 +1,84 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for stream njs module, setting nginx variables.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/stream stream_return/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_set $test_var       test.variable;
+    js_set $test_not_found test.not_found;
+
+    js_import test.js;
+
+    server {
+        listen  127.0.0.1:8081;
+        return  $test_var$status;
+    }
+
+    server {
+        listen  127.0.0.1:8082;
+        return  $test_not_found;
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function variable(s) {
+        s.variables.status = 400;
+        return 'test_var';
+    }
+
+    function not_found(s) {
+        try {
+            s.variables.unknown = 1;
+        } catch (e) {
+            return 'not_found';
+        }
+    }
+
+    export default {variable, not_found};
+
+EOF
+
+$t->try_run('no stream njs available')->plan(2);
+
+###############################################################################
+
+is(stream('127.0.0.1:' . port(8081))->read(), 'test_var400', 'var set');
+is(stream('127.0.0.1:' . port(8082))->read(), 'not_found', 'not found set');
+
+$t->stop();
+
+###############################################################################