blob: bb3b2866589d8483dc7d078d462102f60df76a17 [file] [log] [blame]
#!/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 /chain {
js_content test.chain;
}
location /chunked {
js_content test.chunked;
}
location /header {
js_content test.header;
}
location /multi {
js_content test.multi;
}
location /property {
js_content test.property;
}
}
server {
listen 127.0.0.1:8080;
server_name aaa;
location /loc {
js_content test.loc;
}
location /json { }
}
server {
listen 127.0.0.1:8080;
server_name bbb;
location /loc {
js_content test.loc;
}
}
server {
listen 127.0.0.1:8081;
server_name ccc;
location /loc {
js_content test.loc;
}
}
}
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}`, {headers: {Host: 'aaa'}})
.then(reply => reply[getter]())
.then(data => r.return(200, query(data)))
.catch(e => r.return(501, e.message))
}
function property(r) {
var opts = {headers:{Host: 'aaa'}};
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],
['https://127.0.0.1:$p0/loc'],
];
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', {headers: {Host:'aaa'}}],
['http://127.0.0.1:$p0/loc', {headers: {Host:'bbb'}}],
];
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(r) {
var results = [];
var tests = [
['http://127.0.0.1:$p2/big', {max_response_body_size:128000}],
['http://127.0.0.1:$p2/big/ok', {max_response_body_size:128000}],
['http://127.0.0.1:$p2/chunked'],
['http://127.0.0.1:$p2/chunked/ok'],
['http://127.0.0.1:$p2/chunked/big', {max_response_body_size:128}],
['http://127.0.0.1:$p2/chunked/big'],
];
function collect(v) {
results.push(v);
if (results.length == tests.length) {
results.sort();
r.return(200, JSON.stringify(results));
}
}
tests.forEach(args => {
ngx.fetch.apply(r, args)
.then(reply => reply.text())
.then(body => collect(body.length))
.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))
}
function multi(r) {
var results = [];
var tests = [
[
'http://127.0.0.1:$p0/loc',
{ headers: {Code: 201, Host: 'aaa'}},
],
[
'http://127.0.0.1:$p0/loc',
{ method:'POST', headers: {Code: 401, Host: 'bbb'}, body: 'OK'},
],
[
'http://127.0.0.1:$p1/loc',
{ method:'PATCH',
headers: {foo:undefined, bar:'xxx', Host: 'ccc'}},
],
];
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 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, body, broken, broken_response,
chain, chunked, header, multi, loc, property};
EOF
$t->try_run('no njs.fetch')->plan(27);
$t->run_daemon(\&http_daemon, port(8082));
$t->waitforsocket('127.0.0.1:' . port(8082));
###############################################################################
local $TODO = 'not yet'
unless http_get('/njs') =~ /^([.0-9]+)$/m && $1 ge '0.5.1';
like(http_get('/body?getter=arrayBuffer&loc=loc'), qr/200 OK.*"aaa:GET:::"$/s,
'fetch body arrayBuffer');
like(http_get('/body?getter=text&loc=loc'), qr/200 OK.*"aaa: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('/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":"aaa:GET:::","c":201,"u":"http://127.0.0.1:'.$p0.'/loc"},' .
'{"b":"bbb:POST:::OK","c":401,"u":"http://127.0.0.1:'.$p0.'/loc"},' .
'{"b":"ccc:PATCH::xxx:","c":200,"u":"http://127.0.0.1:'.$p1.'/loc"}]',
'fetch multi');
like(http_get('/multi?throw=1'), qr/500/s, 'fetch destructor');
is(get_json('/broken'),
'[' .
'"connect failed",' .
'"failed to convert url arg",' .
'"invalid url",' .
'"unsupported URL prefix"]', 'fetch broken');
is(get_json('/broken_response'),
'["invalid fetch content length",' .
'"invalid fetch header",' .
'"invalid fetch status line",' .
'"prematurely closed connection",' .
'"prematurely closed connection"]', 'fetch broken response');
is(get_json('/chunked'),
'[10,100010,25500,' .
'"invalid fetch chunked response",' .
'"prematurely closed connection",' .
'"very large fetch chunked response"]', 'fetch chunked');
like(http_get('/chain'), qr/200 OK.*SUCCESS$/s, 'fetch chain');
###############################################################################
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 '/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 '/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;
}
}
}
###############################################################################