view js_subrequests.t @ 1427:eb1d883305ea

Tests: avoid edge cases in upstream random two test. Unavailable servers contribute to the number of attempts, if selected, before the balancer would fall back to the default round-robin method. This means that it's quite possible to get server with more connections. To facilitate with selecting two alive servers, down server was removed from the upstream configuration at the cost of slightly worse coverage.
author Sergey Kandaurov <pluknet@nginx.com>
date Thu, 10 Jan 2019 17:42:34 +0300
parents f168fc46c7a4
children 9d8b100a6ce3
line wrap: on
line source

#!/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_include test.js;

    js_set $async_var async_var;

    server {
        listen       127.0.0.1:8080;
        server_name  localhost;

        location /njs {
            js_content test_njs;
        }

        location /sr {
            js_content sr;
        }

        location /sr_args {
            js_content sr_args;
        }

        location /sr_options_args {
            js_content sr_options_args;
        }

        location /sr_options_method {
            js_content sr_options_method;
        }

        location /sr_options_body {
            js_content sr_options_body;
        }

        location /sr_options_method_head {
            js_content sr_options_method_head;
        }

        location /sr_body {
            js_content sr_body;
        }

        location /sr_body_special {
            js_content sr_body_special;
        }

        location /sr_background {
            js_content sr_background;
        }

        location /sr_in_variable_handler {
            set $_ $async_var;
            js_content sr_in_variable_handler;
        }

        location /sr_error_page {
            set $_ $async_var;
            error_page 404 /return;
            return 404;
        }

        location /sr_js_in_subrequest {
            js_content sr_js_in_subrequest;
        }

        location /sr_file {
            js_content sr_file;
        }

        location /sr_cache {
            js_content sr_cache;
        }


        location /sr_unavail {
            js_content sr_unavail;
        }

        location /sr_broken {
            js_content sr_broken;
        }

        location /sr_too_large {
            js_content sr_too_large;
        }

        location /sr_out_of_order {
            js_content sr_out_of_order;
        }

        location /sr_except_not_a_func {
            js_content sr_except_not_a_func;
        }

        location /sr_except_failed_to_convert_arg {
            js_content sr_except_failed_to_convert_arg;
        }

        location /sr_except_failed_to_convert_options_arg {
            js_content sr_except_failed_to_convert_options_arg;
        }

        location /sr_except_invalid_options_method {
            js_content sr_except_invalid_options_method;
        }

        location /sr_except_invalid_options_header_only {
            js_content sr_except_invalid_options_header_only;
        }

        location /sr_js_in_sr_parent {
            js_content sr_js_in_sr_parent;
        }

        location /sr_in_sr_callback {
            js_content sr_in_sr_callback;
        }

        location /sr_uri_except {
            js_content 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 sr_in_sr;
        }

        location /unavail {
            proxy_pass http://127.0.0.1:8084/;
        }

        location /sr_parent {
             js_content sr_parent;
        }

        location /js_sub {
            js_content js_sub;
        }

        location /return {
            return 200 '["$request_method"]';
        }
    }

    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 body;
        }

        location /background {
            js_content background;
        }

        location /delayed {
            js_content 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(req) {
        subrequest_fn(req, ['/p/sub2'], ['uri', 'status'])
    }

    function sr_args(req, res) {
        req.subrequest('/p/sub1', 'h=xxx', function(reply) {
            res.status = 200;
            res.sendHeader();
            res.send(JSON.stringify({h:reply.headers.h}))
            res.finish();
        });
    }

    function sr_options_args(req, res) {
        req.subrequest('/p/sub1', {args:'h=xxx'}, function(reply) {
            res.status = 200;
            res.sendHeader();
            res.send(JSON.stringify({h:reply.headers.h}))
            res.finish();
        });
    }

    function sr_options_method(req, res) {
        req.subrequest('/p/method', {method:'POST'}, body_fwd_cb);
    }

    function sr_options_body(req, res) {
        req.subrequest('/p/body', {method:'POST', body:'["REQ-BODY"]'},
                       body_fwd_cb);
    }

    function sr_options_method_head(req, res) {
        req.subrequest('/p/method', {method:'HEAD'}, function(reply) {
            res.status = 200;
            res.sendHeader();
            res.send(JSON.stringify({c:reply.status, s:reply.body.length}))
            res.finish();
        });
    }

    function sr_body(req, res) {
        req.subrequest('/p/sub1', body_fwd_cb);
    }

    function sr_body_special(req, res) {
        req.subrequest('/p/sub2', body_fwd_cb);
    }

    function sr_background(req, res) {
        req.subrequest('/p/background');
        req.subrequest('/p/background', 'a=xxx');
        req.subrequest('/p/background', {args: 'a=yyy', method:'POST'});

        res.status = 200;
        res.sendHeader();
        res.finish();
    }

    function body(req, res) {
        res.status = 200;
        res.sendHeader();
        res.send(req.variables.request_body);
        res.finish();
    }

    function delayed(req) {
        setTimeout(function(res) {
                        res.status = 200;
                        res.sendHeader();
                        res.finish();
                   }, 100, req.response);
     }

    function background(req, res) {
        req.log("BACKGROUND: " + req.variables.request_method
                + " args: " + req.variables.args);

        res.status = 200;
        res.sendHeader();
        res.finish();
    }

    function sr_in_variable_handler(req, res) {
    }

    function async_var(req, res) {
        req.subrequest('/p/delayed', function(reply) {
            res.status = 200;
            res.sendHeader();
            res.send(JSON.stringify(["CB-VAR"]))
            res.finish();
        })

        return "";
    }

    function sr_file(req, res) {
        req.subrequest('/file/t', body_fwd_cb);
    }

    function sr_cache(req, res) {
        req.subrequest('/p/t', body_fwd_cb);
    }

    function sr_unavail(req) {
        subrequest_fn(req, ['/unavail'], ['uri', 'status']);
    }

    function sr_broken(req, res) {
        req.subrequest('/daemon/unfinished',
                       function(reply) {
                            res.status = 200;
                            res.sendHeader();
                            res.send(JSON.stringify({code:reply.status}))
                            res.finish();
                        });
    }

    function sr_too_large(req, res) {
        req.subrequest('/too_large/t', body_fwd_cb);
    }

    function sr_in_sr(req, res) {
        req.subrequest('/sr', body_fwd_cb);
    }

    function sr_js_in_subrequest(req, res) {
        req.subrequest('/js_sub', body_fwd_cb);
    }

    function sr_js_in_sr_parent(r) {
        r.subrequest('/sr_parent', 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(req) {
        subrequest_fn(req, ['/p/delayed', '/p/sub1', '/unknown'],
                      ['uri', 'status']);
    }

    function subrequest_fn(req, subs, props) {
        var r, replies = [];

        subs.forEach(function(sr) {
            req.subrequest(sr, function(reply) {
                req.log("subrequest handler: " + reply.uri
                        + " status: " + reply.status)

                r = {};
                props.forEach(function (p) {r[p] = reply[p]});

                replies.push(r);

                if (replies.length == subs.length) {
                    var res = req.response;
                    res.status = 200;
                    res.sendHeader();
                    res.send(JSON.stringify(replies));
                    res.finish();
                }
            });
        });
    }

    function sr_except_not_a_func(req, res) {
        req.subrequest('/sub1', 'a=1', 'b');
    }

    function sr_except_failed_to_convert_arg(req, res) {
        req.subrequest('/sub1', req.args, function(){});
    }

    function sr_except_failed_to_convert_options_arg(req, res) {
        req.subrequest('/sub1', {args:req.args}, function(){});
    }

    function sr_except_invalid_options_method(req, res) {
        req.subrequest('/sub1', {method:'UNKNOWN_METHOD'}, function(){});
    }

    function sr_uri_except(req, res) {
        req.subrequest(req, 'a=1', 'b');
    }

    function body_fwd_cb(reply) {
        var res = reply.parent.response;
        res.status = 200;
        res.sendHeader();
        res.send(JSON.stringify(JSON.parse(reply.body)));
        res.finish();
    }

    function js_sub(req, res) {
        res.status = 200;
        res.sendHeader();
        res.send('["JS-SUB"]');
        res.finish();
    }

EOF

$t->write_file('t', '["SEE-THIS"]');

$t->try_run('no njs available')->plan(25);
$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'), '["POST"]', 'sr_options_method');
is(get_json('/sr_options_body'), '["REQ-BODY"]', 'sr_options_body');
is(get_json('/sr_options_method_head'), '{"c":200,"s":0}',
	'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');

http_get('/sr_background');

http_get('/sr_broken');
http_get('/sr_in_sr');
http_get('/sr_in_variable_handler');
http_get('/sr_error_page');
http_get('/sr_too_large');
http_get('/sr_except_not_a_func');
http_get('/sr_except_failed_to_convert_arg');
http_get('/sr_except_failed_to_convert_options_arg');
http_get('/sr_except_invalid_options_method');
http_get('/sr_uri_except');

TODO: {
local $TODO = 'not yet'
	unless http_get('/njs') =~ /^([.0-9]+)$/m && $1 ge '0.2.4';

is(get_json('/sr_js_in_sr_parent'),
	'{"e":"parent can only be returned for a subrequest"}',
	'parent in subrequest js_content');

todo_skip 'leaves coredump', 1
	unless http_get('/njs') =~ /^([.0-9]+)$/m && $1 ge '0.2.4';

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 args') > 0,
	'subrequest invalid args exception');
ok(index($t->read_file('error.log'), 'unknown method "UNKNOWN_METHOD"') > 0,
	'subrequest unknown method exception');
ok(index($t->read_file('error.log'), 'BACKGROUND') > 0,
	'background subrequest');
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');

###############################################################################

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);
		}
	}
}

###############################################################################