aboutsummaryrefslogtreecommitdiff
path: root/nginx
diff options
context:
space:
mode:
authorDmitry Volyntsev <xeioex@nginx.com>2023-05-22 17:59:47 -0700
committerDmitry Volyntsev <xeioex@nginx.com>2023-05-22 17:59:47 -0700
commit7c39d2c23a2152f99f1602a0cb81903c6d15e980 (patch)
tree68292d76ac4b64a458ac6927909355b60b5bb4ee /nginx
parent3cf640f5a041bada0d39d32a27494ffe024767ef (diff)
downloadnjs-7c39d2c23a2152f99f1602a0cb81903c6d15e980.tar.gz
njs-7c39d2c23a2152f99f1602a0cb81903c6d15e980.zip
Tests: imported nginx modules tests from nginx-tests.
Diffstat (limited to 'nginx')
-rw-r--r--nginx/t/README12
-rw-r--r--nginx/t/js.t391
-rw-r--r--nginx/t/js_args.t167
-rw-r--r--nginx/t/js_async.t225
-rw-r--r--nginx/t/js_body_filter.t168
-rw-r--r--nginx/t/js_body_filter_if.t127
-rw-r--r--nginx/t/js_buffer.t184
-rw-r--r--nginx/t/js_dump.t110
-rw-r--r--nginx/t/js_fetch.t710
-rw-r--r--nginx/t/js_fetch_https.t283
-rw-r--r--nginx/t/js_fetch_objects.t500
-rw-r--r--nginx/t/js_fetch_resolver.t231
-rw-r--r--nginx/t/js_fetch_timeout.t119
-rw-r--r--nginx/t/js_fetch_verify.t192
-rw-r--r--nginx/t/js_header_filter.t93
-rw-r--r--nginx/t/js_header_filter_if.t95
-rw-r--r--nginx/t/js_headers.t568
-rw-r--r--nginx/t/js_import.t108
-rw-r--r--nginx/t/js_import2.t127
-rw-r--r--nginx/t/js_internal_redirect.t107
-rw-r--r--nginx/t/js_modules.t84
-rw-r--r--nginx/t/js_ngx.t94
-rw-r--r--nginx/t/js_object.t137
-rw-r--r--nginx/t/js_paths.t110
-rw-r--r--nginx/t/js_preload_object.t181
-rw-r--r--nginx/t/js_promise.t201
-rw-r--r--nginx/t/js_request_body.t110
-rw-r--r--nginx/t/js_return.t73
-rw-r--r--nginx/t/js_subrequests.t636
-rw-r--r--nginx/t/js_var.t90
-rw-r--r--nginx/t/js_var2.t90
-rw-r--r--nginx/t/js_variables.t95
-rw-r--r--nginx/t/stream_js.t478
-rw-r--r--nginx/t/stream_js_buffer.t177
-rw-r--r--nginx/t/stream_js_exit.t152
-rw-r--r--nginx/t/stream_js_fetch.t277
-rw-r--r--nginx/t/stream_js_fetch_https.t404
-rw-r--r--nginx/t/stream_js_fetch_init.t149
-rw-r--r--nginx/t/stream_js_import.t117
-rw-r--r--nginx/t/stream_js_import2.t117
-rw-r--r--nginx/t/stream_js_ngx.t94
-rw-r--r--nginx/t/stream_js_object.t98
-rw-r--r--nginx/t/stream_js_preload_object.t122
-rw-r--r--nginx/t/stream_js_send.t186
-rw-r--r--nginx/t/stream_js_var.t75
-rw-r--r--nginx/t/stream_js_var2.t75
-rw-r--r--nginx/t/stream_js_variables.t84
47 files changed, 9023 insertions, 0 deletions
diff --git a/nginx/t/README b/nginx/t/README
new file mode 100644
index 00000000..ed1e6c59
--- /dev/null
+++ b/nginx/t/README
@@ -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
index 00000000..5f2dcda8
--- /dev/null
+++ b/nginx/t/js.t
@@ -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
index 00000000..bb5cf4ff
--- /dev/null
+++ b/nginx/t/js_args.t
@@ -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
index 00000000..73e71851
--- /dev/null
+++ b/nginx/t/js_async.t
@@ -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
index 00000000..b90a2502
--- /dev/null
+++ b/nginx/t/js_body_filter.t
@@ -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
index 00000000..af0aa865
--- /dev/null
+++ b/nginx/t/js_body_filter_if.t
@@ -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
index 00000000..ce4c15d0
--- /dev/null
+++ b/nginx/t/js_buffer.t
@@ -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
index 00000000..c00a53a2
--- /dev/null
+++ b/nginx/t/js_dump.t
@@ -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
index 00000000..428be90f
--- /dev/null
+++ b/nginx/t/js_fetch.t
@@ -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
index 00000000..154bbbcc
--- /dev/null
+++ b/nginx/t/js_fetch_https.t
@@ -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
index 00000000..9f23599c
--- /dev/null
+++ b/nginx/t/js_fetch_objects.t
@@ -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
index 00000000..8b0dc450
--- /dev/null
+++ b/nginx/t/js_fetch_resolver.t
@@ -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
index 00000000..486656d6
--- /dev/null
+++ b/nginx/t/js_fetch_timeout.t
@@ -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
index 00000000..d6bb1d9e
--- /dev/null
+++ b/nginx/t/js_fetch_verify.t
@@ -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
index 00000000..aecac34d
--- /dev/null
+++ b/nginx/t/js_header_filter.t
@@ -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
index 00000000..a6ab3c42
--- /dev/null
+++ b/nginx/t/js_header_filter_if.t
@@ -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
index 00000000..6f08dcff
--- /dev/null
+++ b/nginx/t/js_headers.t
@@ -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
index 00000000..52ee7954
--- /dev/null
+++ b/nginx/t/js_import.t
@@ -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
index 00000000..cd29d2dc
--- /dev/null
+++ b/nginx/t/js_import2.t
@@ -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
index 00000000..ec6be4e1
--- /dev/null
+++ b/nginx/t/js_internal_redirect.t
@@ -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
index 00000000..21581b4a
--- /dev/null
+++ b/nginx/t/js_modules.t
@@ -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
index 00000000..c6271f08
--- /dev/null
+++ b/nginx/t/js_ngx.t
@@ -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
index 00000000..97e778a2
--- /dev/null
+++ b/nginx/t/js_object.t
@@ -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
index 00000000..98d87512
--- /dev/null
+++ b/nginx/t/js_paths.t
@@ -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
index 00000000..407e97fe
--- /dev/null
+++ b/nginx/t/js_preload_object.t
@@ -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
index 00000000..f6084e99
--- /dev/null
+++ b/nginx/t/js_promise.t
@@ -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
index 00000000..350e7fb8
--- /dev/null
+++ b/nginx/t/js_request_body.t
@@ -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
index 00000000..2cc32f74
--- /dev/null
+++ b/nginx/t/js_return.t
@@ -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
index 00000000..a2007fa5
--- /dev/null
+++ b/nginx/t/js_subrequests.t
@@ -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
index 00000000..4de8236c
--- /dev/null
+++ b/nginx/t/js_var.t
@@ -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
index 00000000..5c59d9cc
--- /dev/null
+++ b/nginx/t/js_var2.t
@@ -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
index 00000000..f2481e0b
--- /dev/null
+++ b/nginx/t/js_variables.t
@@ -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
index 00000000..ff92aad2
--- /dev/null
+++ b/nginx/t/stream_js.t
@@ -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
index 00000000..cc960136
--- /dev/null
+++ b/nginx/t/stream_js_buffer.t
@@ -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
index 00000000..a8bc34ae
--- /dev/null
+++ b/nginx/t/stream_js_exit.t
@@ -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
index 00000000..106702dc
--- /dev/null
+++ b/nginx/t/stream_js_fetch.t
@@ -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
index 00000000..056352f4
--- /dev/null
+++ b/nginx/t/stream_js_fetch_https.t
@@ -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
index 00000000..6de487da
--- /dev/null
+++ b/nginx/t/stream_js_fetch_init.t
@@ -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
index 00000000..70000604
--- /dev/null
+++ b/nginx/t/stream_js_import.t
@@ -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
index 00000000..a057c258
--- /dev/null
+++ b/nginx/t/stream_js_import2.t
@@ -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
index 00000000..de6dc58a
--- /dev/null
+++ b/nginx/t/stream_js_ngx.t
@@ -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
index 00000000..504b9348
--- /dev/null
+++ b/nginx/t/stream_js_object.t
@@ -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
index 00000000..c28c401a
--- /dev/null
+++ b/nginx/t/stream_js_preload_object.t
@@ -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
index 00000000..318be6a6
--- /dev/null
+++ b/nginx/t/stream_js_send.t
@@ -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
index 00000000..cde2dc9f
--- /dev/null
+++ b/nginx/t/stream_js_var.t
@@ -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
index 00000000..71cee1e3
--- /dev/null
+++ b/nginx/t/stream_js_var2.t
@@ -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
index 00000000..29e6c33e
--- /dev/null
+++ b/nginx/t/stream_js_variables.t
@@ -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();
+
+###############################################################################