# vim:set ft= ts=4 sw=4 et fdm=marker:

use Test::Nginx::Socket::Lua;

#repeat_each(20000);
#repeat_each(200);
repeat_each(2);
#master_on();
#workers(1);
#log_level('debug');
#log_level('warn');
#worker_connections(1024);

plan tests => repeat_each() * (blocks() * 3 + 2);

$ENV{TEST_NGINX_MEMCACHED_PORT} ||= 11211;
$ENV{TEST_NGINX_MYSQL_PORT} ||= 3306;

our $LuaCpath = $ENV{LUA_CPATH} ||
    '/usr/local/openresty-debug/lualib/?.so;/usr/local/openresty/lualib/?.so;;';

#$ENV{LUA_PATH} = $ENV{HOME} . '/work/JSON4Lua-0.9.30/json/?.lua';

no_long_string();

run_tests();

__DATA__

=== TEST 1: throw 403
--- config
    location /lua {
        content_by_lua "ngx.exit(403);ngx.say('hi')";
    }
--- request
GET /lua
--- error_code: 403
--- response_body_like: 403 Forbidden
--- no_error_log
[error]



=== TEST 2: throw 404
--- config
    location /lua {
        content_by_lua "ngx.exit(404);ngx.say('hi');";
    }
--- request
GET /lua
--- error_code: 404
--- response_body_like: 404 Not Found
--- no_error_log
[error]



=== TEST 3: throw 404 after sending the header and partial body
--- config
    location /lua {
        content_by_lua "ngx.say('hi');ngx.exit(404);ngx.say(', you')";
    }
--- request
GET /lua
--- error_log
attempt to set status 404 via ngx.exit after sending out the response status 200
--- no_error_log
[alert]
--- response_body
hi



=== TEST 4: working with ngx_auth_request (succeeded)
--- config
    location /auth {
        content_by_lua "
            if ngx.var.user == 'agentzh' then
                ngx.eof();
            else
                ngx.exit(403)
            end";
    }
    location /api {
        set $user $arg_user;
        auth_request /auth;

        echo "Logged in";
    }
--- request
GET /api?user=agentzh
--- error_code: 200
--- response_body
Logged in
--- no_error_log
[error]



=== TEST 5: working with ngx_auth_request (failed)
--- config
    location /auth {
        content_by_lua "
            if ngx.var.user == 'agentzh' then
                ngx.eof();
            else
                ngx.exit(403)
            end";
    }
    location /api {
        set $user $arg_user;
        auth_request /auth;

        echo "Logged in";
    }
--- request
GET /api?user=agentz
--- error_code: 403
--- response_body_like: 403 Forbidden
--- no_error_log
[error]



=== TEST 6: working with ngx_auth_request (simplest form, w/o ngx_memc)
--- skip_eval: 3:$ENV{TEST_NGINX_USE_HTTP3}
--- no_http2
--- http_config eval
"
    lua_package_cpath '$::LuaCpath';
    upstream backend {
        drizzle_server 127.0.0.1:\$TEST_NGINX_MYSQL_PORT protocol=mysql
                       dbname=ngx_test user=ngx_test password=ngx_test;
        drizzle_keepalive max=300 mode=single overflow=ignore;
    }
"
--- config
    location /memc {
        internal;

        set $memc_key $arg_key;
        set $memc_exptime $arg_exptime;

        memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT;
    }

    location /conv-uid-mysql {
        internal;

        set $key "conv-uid-$arg_uid";

        #srcache_fetch GET /memc key=$key;
        #srcache_store PUT /memc key=$key;

        default_type 'application/json';

        drizzle_query "select new_uid as uid from conv_uid where old_uid=$arg_uid";
        drizzle_pass backend;

        rds_json on;
    }

    location /conv-uid {
        internal;
        content_by_lua_file 'html/foo.lua';
    }
    location /api {
        set $uid $arg_uid;
        auth_request /conv-uid;

        echo "Logged in $uid";
    }
--- user_files
>>> foo.lua
local cjson = require('cjson');
local old_uid = ngx.var.uid
-- print('about to run sr')
local res = ngx.location.capture('/conv-uid-mysql?uid=' .. old_uid)
if (res.status ~= ngx.HTTP_OK) then
    ngx.exit(res.status)
end
-- print('just have run sr: ' .. res.body)
res = cjson.decode(res.body)
if (not res or not res[1] or not res[1].uid or
        not string.match(res[1].uid, '^%d+$')) then
    ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
ngx.var.uid = res[1].uid;
-- print('done')
--- request
GET /api?uid=32
--- response_body
Logged in 56
--- no_error_log
[error]



=== TEST 7: working with ngx_auth_request (simplest form)
--- skip_eval: 3:$ENV{TEST_NGINX_USE_HTTP3}
--- no_http2
--- http_config eval
"
    lua_package_cpath '$::LuaCpath';
    upstream backend {
        drizzle_server 127.0.0.1:\$TEST_NGINX_MYSQL_PORT protocol=mysql
                       dbname=ngx_test user=ngx_test password=ngx_test;
        drizzle_keepalive max=300 mode=single overflow=ignore;
    }
"
--- config
    location /memc {
        internal;

        set $memc_key $arg_key;
        set $memc_exptime $arg_exptime;

        memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT;
    }

    location /conv-uid-mysql {
        internal;

        set $key "conv-uid-$arg_uid";

        #srcache_fetch GET /memc key=$key;
        #srcache_store PUT /memc key=$key;

        default_type 'application/json';

        drizzle_query "select new_uid as uid from conv_uid where old_uid=$arg_uid";
        drizzle_pass backend;

        rds_json on;
    }

    location /conv-uid {
        internal;
        content_by_lua_file 'html/foo.lua';
    }
    location /api {
        set $uid $arg_uid;
        auth_request /conv-uid;

        echo "Logged in $uid";
    }
--- user_files
>>> foo.lua
local cjson = require('cjson');
local old_uid = ngx.var.uid
-- print('about to run sr')
local res = ngx.location.capture('/conv-uid-mysql?uid=' .. old_uid)
-- print('just have run sr' .. res.body)
if (res.status ~= ngx.HTTP_OK) then
    ngx.exit(res.status)
end
res = cjson.decode(res.body)
if (not res or not res[1] or not res[1].uid or
        not string.match(res[1].uid, '^%d+$')) then
    ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
ngx.var.uid = res[1].uid;
-- print('done')
--- request
GET /api?uid=32
--- response_body
Logged in 56
--- no_error_log
[error]



=== TEST 8: working with ngx_auth_request
--- no_http2
--- skip_eval: 3:$ENV{TEST_NGINX_USE_HTTP3}
--- http_config eval
"
    lua_package_cpath '$::LuaCpath';
    upstream backend {
        drizzle_server 127.0.0.1:\$TEST_NGINX_MYSQL_PORT protocol=mysql
                       dbname=ngx_test user=ngx_test password=ngx_test;
        drizzle_keepalive max=300 mode=single overflow=ignore;
    }

    upstream memc_a {
        server 127.0.0.1:\$TEST_NGINX_MEMCACHED_PORT;
    }

    upstream memc_b {
        server 127.0.0.1:\$TEST_NGINX_MEMCACHED_PORT;
    }

    upstream_list memc_cluster memc_a memc_b;
"
--- config
    location /memc {
        internal;

        set $memc_key $arg_key;
        set $memc_exptime $arg_exptime;

        set_hashed_upstream $backend memc_cluster $arg_key;
        memc_pass $backend;
    }

    location /conv-uid-mysql {
        internal;

        set $key "conv-uid-$arg_uid";

        #srcache_fetch GET /memc key=$key;
        #srcache_store PUT /memc key=$key;

        default_type 'application/json';

        drizzle_query "select new_uid as uid from conv_uid where old_uid=$arg_uid";
        drizzle_pass backend;

        rds_json on;
    }

    location /conv-uid {
        internal;
        content_by_lua_file 'html/foo.lua';
    }
    location /api {
        set $uid $arg_uid;
        auth_request /conv-uid;

        echo "Logged in $uid";
    }
--- user_files
>>> foo.lua
local cjson = require('cjson');
local old_uid = ngx.var.uid
-- print('about to run sr')
local res = ngx.location.capture('/conv-uid-mysql?uid=' .. old_uid)
-- print('just have run sr' .. res.body)
if (res.status ~= ngx.HTTP_OK) then
    ngx.exit(res.status)
end
res = cjson.decode(res.body)
if (not res or not res[1] or not res[1].uid or
        not string.match(res[1].uid, '^%d+$')) then
    ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
ngx.var.uid = res[1].uid;
-- print('done')
--- request
GET /api?uid=32
--- response_body
Logged in 56
--- no_error_log
[error]
--- timeout: 5



=== TEST 9: working with ngx_auth_request
--- http_config
    upstream backend {
        drizzle_server 127.0.0.1:$TEST_NGINX_MYSQL_PORT protocol=mysql
                       dbname=ngx_test user=ngx_test password=ngx_test;
        drizzle_keepalive max=300 mode=single overflow=ignore;
    }

    upstream memc_a {
        server 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT;
        keepalive 300;
    }

    #upstream_list memc_cluster memc_a memc_b;

--- config
    location /memc {
        internal;

        set $memc_key $arg_key;
        set $memc_exptime $arg_exptime;

        #set_hashed_upstream $backend memc_cluster $arg_key;
        memc_pass memc_a;
    }

    location /conv-mysql {
        internal;

        set $key "conv-uri-$query_string";

        #srcache_fetch GET /memc key=$key;
        #srcache_store PUT /memc key=$key;

        default_type 'application/json';

        set_quote_sql_str $seo_uri $query_string;
        drizzle_query "select url from my_url_map where seo_url=$seo_uri";
        drizzle_pass backend;

        rds_json on;
    }

    location /conv-uid {
        internal;
        content_by_lua_file 'html/foo.lua';
    }

    location /baz {
        set $my_uri $uri;
        auth_request /conv-uid;

        echo_exec /jump $my_uri;
    }

    location /jump {
        internal;
        rewrite ^ $query_string? redirect;
    }
--- user_files
>>> foo.lua
local cjson = require('cjson');
local seo_uri = ngx.var.my_uri
-- print('about to run sr')
local res = ngx.location.capture('/conv-mysql?' .. seo_uri)
if (res.status ~= ngx.HTTP_OK) then
    ngx.exit(res.status)
end
res = cjson.decode(res.body)
if (not res or not res[1] or not res[1].url) then
    ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
ngx.var.my_uri = res[1].url;
-- print('done')
--- request
GET /baz
--- response_body_like: 302
--- error_code: 302
--- response_headers
Location: http://localhost:$ServerPort/foo/bar
--- SKIP



=== TEST 10: throw 0
--- config
    location /lua {
        content_by_lua "ngx.say('Hi'); ngx.eof(); ngx.exit(0);ngx.say('world')";
    }
--- request
GET /lua
--- error_code: 200
--- response_body
Hi
--- no_error_log
[error]



=== TEST 11: pcall safe
--- config
    location /lua {
        content_by_lua '
            local function f ()
                ngx.say("hello")
                ngx.exit(200)
            end

            pcall(f)
            ngx.say("world")
        ';
    }
--- request
GET /lua
--- error_code: 200
--- response_body
hello
--- no_error_log
[error]



=== TEST 12: 501 Method Not Implemented
--- config
    location /lua {
        content_by_lua '
            ngx.exit(501)
        ';
    }
--- request
GET /lua
--- error_code: 501
--- response_body_like: 501 (?:Method )?Not Implemented
--- no_error_log
[error]



=== TEST 13: 501 Method Not Implemented
--- config
    location /lua {
        content_by_lua '
            ngx.exit(ngx.HTTP_METHOD_NOT_IMPLEMENTED)
        ';
    }
--- request
GET /lua
--- error_code: 501
--- response_body_like: 501 (?:Method )?Not Implemented
--- no_error_log
[error]



=== TEST 14: throw 403 after sending out headers with 200
--- config
    location /lua {
        rewrite_by_lua '
            ngx.send_headers()
            ngx.say("Hello World")
            ngx.exit(403)
        ';
    }
--- request
GET /lua
--- response_body
Hello World
--- error_log
attempt to set status 403 via ngx.exit after sending out the response status 200
--- no_error_log
[alert]



=== TEST 15: throw 403 after sending out headers with 403
--- config
    location /lua {
        rewrite_by_lua '
            ngx.status = 403
            ngx.send_headers()
            ngx.say("Hello World")
            ngx.exit(403)
        ';
    }
--- request
GET /lua
--- response_body
Hello World
--- error_code: 403
--- no_error_log
[error]
[alert]



=== TEST 16: throw 403 after sending out headers with 403 (HTTP 1.0 buffering)
--- config
    location /t {
        rewrite_by_lua '
            ngx.status = 403
            ngx.say("Hello World")
            ngx.exit(403)
        ';
    }
--- request
GET /t HTTP/1.0
--- response_body
Hello World
--- error_code: 403
--- no_error_log
[error]
[alert]



=== TEST 17: throw 444 after sending out responses (HTTP 1.0)
--- config
    location /lua {
        content_by_lua "
            ngx.say('ok');
            return ngx.exit(444)
        ";
    }
--- request
GET /lua HTTP/1.0
--- ignore_response
--- log_level: debug
--- no_error_log
lua sending HTTP 1.0 response headers
[error]



=== TEST 18: throw 499 after sending out responses (HTTP 1.0)
--- config
    location /lua {
        content_by_lua "
            ngx.say('ok');
            return ngx.exit(499)
        ";
    }
--- request
GET /lua HTTP/1.0
--- ignore_response
--- log_level: debug
--- no_error_log
lua sending HTTP 1.0 response headers
[error]



=== TEST 19: throw 408 after sending out responses (HTTP 1.0)
--- config
    location /lua {
        content_by_lua "
            ngx.say('ok');
            return ngx.exit(408)
        ";
    }
--- request
GET /lua HTTP/1.0
--- ignore_response
--- log_level: debug
--- no_error_log
lua sending HTTP 1.0 response headers
[error]



=== TEST 20: exit(201) with custom response body
--- config
    location = /t {
        content_by_lua "
            ngx.status = 201
            ngx.say('ok');
            return ngx.exit(201)
        ";
    }
--- request
    GET /t
--- ignore_response
--- log_level: debug
--- no_error_log
lua sending HTTP 1.0 response headers
[error]
[alert]



=== TEST 21: exit 403 in header filter
--- config
    location = /t {
        content_by_lua "ngx.say('hi');";
        header_filter_by_lua '
            return ngx.exit(403)
        ';
    }
--- request
GET /t
--- error_code: 403
--- response_body_like: 403 Forbidden
--- no_error_log
[error]
--- skip_eval: 3:defined($ENV{MOCKEAGAIN}) && ($ENV{MOCKEAGAIN} =~ /w/)



=== TEST 22: exit 201 in header filter
--- config
    lingering_close always;
    location = /t {
        content_by_lua "ngx.say('hi');";
        header_filter_by_lua '
            return ngx.exit(201)
        ';
    }
--- request
GET /t
--- error_code: 201
--- response_body
--- no_error_log
[error]
--- skip_eval: 3:defined($ENV{MOCKEAGAIN}) && ($ENV{MOCKEAGAIN} =~ /w/)



=== TEST 23: exit both in header filter and content handler
--- config
    location = /t {
        content_by_lua "ngx.status = 201 ngx.say('hi') ngx.exit(201)";
        header_filter_by_lua '
            return ngx.exit(201)
        ';
    }
--- request
GET /t
--- error_code: 201
--- stap2
/*
F(ngx_http_send_header) {
    printf("=== %d\n", $r->headers_out->status)
    print_ubacktrace()
}
*/
F(ngx_http_lua_header_filter_inline) {
    printf("=== %d\n", $r->headers_out->status)
    print_ubacktrace()
}
F(ngx_http_lua_header_filter_by_chunk).return {
    if ($return == -1) {
        printf("====== header filter by chunk\n")
        print_ubacktrace()
    }
}
--- stap_out
--- response_body
--- no_error_log
[error]
[alert]
--- skip_eval: 4:defined($ENV{MOCKEAGAIN}) && ($ENV{MOCKEAGAIN} =~ /w/)



=== TEST 24: exit 444 in header filter
--- config
    location = /t {
        content_by_lua "ngx.say('hello world');";
        header_filter_by_lua '
            return ngx.exit(444)
        ';
    }
--- request
GET /t
--- error_code: 444
--- response_body
--- no_error_log
[error]
--- skip_eval: 3:defined($ENV{MOCKEAGAIN}) && ($ENV{MOCKEAGAIN} =~ /w/)



=== TEST 25: 501 Method Not Implemented
--- config
    location /lua {
        content_by_lua '
            ngx.exit(ngx.HTTP_NOT_IMPLEMENTED)
        ';
    }
--- request
GET /lua
--- error_code: 501
--- response_body_like: 501 (?:Method )?Not Implemented
--- no_error_log
[error]



=== TEST 26: accepts NGX_OK
--- config
    location = /t {
        content_by_lua_block {
            ngx.exit(ngx.OK)
        }
    }
--- request
GET /t
--- response_body
--- no_error_log
[error]



=== TEST 27: accepts NGX_ERROR
--- no_http2
--- config
    location = /t {
        content_by_lua_block {
            ngx.exit(ngx.ERROR)
        }
    }
--- request
GET /t
--- error_code:
--- response_body
--- no_error_log
[error]
--- curl_error
curl: (95) HTTP/3 stream 0 reset by server



=== TEST 28: accepts NGX_DECLINED
--- no_http2
--- config
    location = /t {
        content_by_lua_block {
            ngx.exit(ngx.DECLINED)
        }
    }
--- request
GET /t
--- error_code:
--- response_body
--- no_error_log
[error]
--- curl_error
curl: (95) HTTP/3 stream 0 reset by server



=== TEST 29: refuses NGX_AGAIN
--- config
    location = /t {
        content_by_lua_block {
            ngx.exit(ngx.AGAIN)
        }
    }
--- request
GET /t
--- error_code: 500
--- response_body_like: 500 Internal Server Error
--- error_log eval
qr/\[error\] .*? bad argument to 'ngx.exit': does not accept NGX_AGAIN or NGX_DONE/



=== TEST 30: refuses NGX_DONE
--- config
    location = /t {
        content_by_lua_block {
            ngx.exit(ngx.DONE)
        }
    }
--- request
GET /t
--- error_code: 500
--- response_body_like: 500 Internal Server Error
--- error_log eval
qr/\[error\] .*? bad argument to 'ngx.exit': does not accept NGX_AGAIN or NGX_DONE/
