diff options
author | Loïc Hoguin <[email protected]> | 2016-08-10 11:49:31 +0200 |
---|---|---|
committer | Loïc Hoguin <[email protected]> | 2016-08-10 11:49:31 +0200 |
commit | ae0dd616737d8e1116de4a04be0bc84188997eb0 (patch) | |
tree | 57c95ae977f6b6c49c0fe06e4bb68157815faa46 /test | |
parent | 0ba3a9a22269d21b2962fec78c03e5671294d20d (diff) | |
download | cowboy-ae0dd616737d8e1116de4a04be0bc84188997eb0.tar.gz cowboy-ae0dd616737d8e1116de4a04be0bc84188997eb0.tar.bz2 cowboy-ae0dd616737d8e1116de4a04be0bc84188997eb0.zip |
Add tests for responses and request body reading
This is a large commit. The cowboy_req interface has largely
changed, and will change a little more. It's possible that
some examples or tests have not been converted to the new
interface yet. The documentation has not yet been updated.
All of this will be fixed in smaller subsequent commits.
Gotta start somewhere...
Diffstat (limited to 'test')
-rw-r--r-- | test/handlers/echo_h.erl | 32 | ||||
-rw-r--r-- | test/handlers/multipart_h.erl | 65 | ||||
-rw-r--r-- | test/handlers/resp_h.erl | 158 | ||||
-rw-r--r-- | test/http_SUITE_data/http_body_qs.erl | 2 | ||||
-rw-r--r-- | test/http_SUITE_data/http_loop_stream_recv.erl | 2 | ||||
-rw-r--r-- | test/req_SUITE.erl | 444 |
6 files changed, 692 insertions, 11 deletions
diff --git a/test/handlers/echo_h.erl b/test/handlers/echo_h.erl index fd45c5f..98594dc 100644 --- a/test/handlers/echo_h.erl +++ b/test/handlers/echo_h.erl @@ -12,10 +12,32 @@ init(Req, Opts) -> echo_arg(Arg, Req, Opts) end. -echo(<<"body">>, Req0, Opts) -> - {ok, Body, Req} = cowboy_req:read_body(Req0), +echo(<<"read_body">>, Req0, Opts) -> + case Opts of + #{crash := true} -> ct_helper:ignore(cowboy_req, read_body, 2); + _ -> ok + end, + {_, Body, Req} = case cowboy_req:path(Req0) of + <<"/full", _/bits>> -> read_body(Req0, <<>>); + <<"/opts", _/bits>> -> cowboy_req:read_body(Req0, Opts); + _ -> cowboy_req:read_body(Req0) + end, cowboy_req:reply(200, #{}, Body, Req), {ok, Req, Opts}; +echo(<<"read_urlencoded_body">>, Req0, Opts) -> + Path = cowboy_req:path(Req0), + case {Path, Opts} of + {<<"/opts", _/bits>>, #{crash := true}} -> ct_helper:ignore(cowboy_req, read_body, 2); + {_, #{crash := true}} -> ct_helper:ignore(cowboy_req, read_urlencoded_body, 2); + _ -> ok + end, + {ok, Body, Req} = case Path of + <<"/opts", _/bits>> -> cowboy_req:read_urlencoded_body(Req0, Opts); + <<"/crash", _/bits>> -> cowboy_req:read_urlencoded_body(Req0, Opts); + _ -> cowboy_req:read_urlencoded_body(Req0) + end, + cowboy_req:reply(200, #{}, value_to_iodata(Body), Req), + {ok, Req, Opts}; echo(<<"uri">>, Req, Opts) -> Value = case cowboy_req:path_info(Req) of [<<"origin">>] -> cowboy_req:uri(Req, #{host => undefined}); @@ -55,6 +77,12 @@ echo_arg(Arg0, Req, Opts) -> cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), {ok, Req, Opts}. +read_body(Req0, Acc) -> + case cowboy_req:read_body(Req0) of + {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req}; + {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>) + end. + value_to_iodata(V) when is_integer(V) -> integer_to_binary(V); value_to_iodata(V) when is_atom(V) -> atom_to_binary(V, latin1); value_to_iodata(V) when is_list(V); is_tuple(V); is_map(V) -> io_lib:format("~p", [V]); diff --git a/test/handlers/multipart_h.erl b/test/handlers/multipart_h.erl new file mode 100644 index 0000000..289d2ed --- /dev/null +++ b/test/handlers/multipart_h.erl @@ -0,0 +1,65 @@ +%% This module reads a multipart body and echoes it back as an Erlang term. + +-module(multipart_h). + +-export([init/2]). + +init(Req0, State) -> + {Result, Req} = case cowboy_req:binding(key, Req0) of + undefined -> acc_multipart(Req0, []); + <<"skip_body">> -> skip_body_multipart(Req0, []); + <<"read_part2">> -> read_part2_multipart(Req0, []); + <<"read_part_body2">> -> read_part_body2_multipart(Req0, []) + end, + {ok, cowboy_req:reply(200, #{}, term_to_binary(Result), Req), State}. + +acc_multipart(Req0, Acc) -> + case cowboy_req:part(Req0) of + {ok, Headers, Req1} -> + {ok, Body, Req} = stream_body(Req1, <<>>), + acc_multipart(Req, [{Headers, Body}|Acc]); + {done, Req} -> + {lists:reverse(Acc), Req} + end. + +stream_body(Req0, Acc) -> + case cowboy_req:part_body(Req0) of + {more, Data, Req} -> + stream_body(Req, << Acc/binary, Data/binary >>); + {ok, Data, Req} -> + {ok, << Acc/binary, Data/binary >>, Req} + end. + +skip_body_multipart(Req0, Acc) -> + case cowboy_req:part(Req0) of + {ok, Headers, Req} -> + skip_body_multipart(Req, [Headers|Acc]); + {done, Req} -> + {lists:reverse(Acc), Req} + end. + +read_part2_multipart(Req0, Acc) -> + case cowboy_req:part(Req0, #{length => 1, period => 1}) of + {ok, Headers, Req1} -> + {ok, Body, Req} = stream_body(Req1, <<>>), + acc_multipart(Req, [{Headers, Body}|Acc]); + {done, Req} -> + {lists:reverse(Acc), Req} + end. + +read_part_body2_multipart(Req0, Acc) -> + case cowboy_req:part(Req0) of + {ok, Headers, Req1} -> + {ok, Body, Req} = stream_body2(Req1, <<>>), + acc_multipart(Req, [{Headers, Body}|Acc]); + {done, Req} -> + {lists:reverse(Acc), Req} + end. + +stream_body2(Req0, Acc) -> + case cowboy_req:part_body(Req0, #{length => 1, period => 1}) of + {more, Data, Req} -> + stream_body(Req, << Acc/binary, Data/binary >>); + {ok, Data, Req} -> + {ok, << Acc/binary, Data/binary >>, Req} + end. diff --git a/test/handlers/resp_h.erl b/test/handlers/resp_h.erl new file mode 100644 index 0000000..36e6f13 --- /dev/null +++ b/test/handlers/resp_h.erl @@ -0,0 +1,158 @@ +%% This module echoes back the value the test is interested in. + +-module(resp_h). + +-export([init/2]). + +init(Req, Opts) -> + do(cowboy_req:binding(key, Req), Req, Opts). + +do(<<"set_resp_cookie3">>, Req0, Opts) -> + Req = case cowboy_req:binding(arg, Req0) of + undefined -> + cowboy_req:set_resp_cookie(<<"mycookie">>, "myvalue", Req0); + <<"multiple">> -> + Req1 = cowboy_req:set_resp_cookie(<<"mycookie">>, "myvalue", Req0), + cowboy_req:set_resp_cookie(<<"yourcookie">>, <<"yourvalue">>, Req1); + <<"overwrite">> -> + Req1 = cowboy_req:set_resp_cookie(<<"mycookie">>, "myvalue", Req0), + cowboy_req:set_resp_cookie(<<"mycookie">>, <<"overwrite">>, Req1) + end, + cowboy_req:reply(200, #{}, "OK", Req), + {ok, Req, Opts}; +do(<<"set_resp_cookie4">>, Req0, Opts) -> + Req = cowboy_req:set_resp_cookie(<<"mycookie">>, "myvalue", #{path => cowboy_req:path(Req0)}, Req0), + cowboy_req:reply(200, #{}, "OK", Req), + {ok, Req, Opts}; +do(<<"set_resp_header">>, Req0, Opts) -> + Req = cowboy_req:set_resp_header(<<"content-type">>, <<"text/plain">>, Req0), + cowboy_req:reply(200, #{}, "OK", Req), + {ok, Req, Opts}; +do(<<"set_resp_body">>, Req0, Opts) -> + Arg = cowboy_req:binding(arg, Req0), + Req = case Arg of + <<"sendfile">> -> + AppFile = code:where_is_file("cowboy.app"), + cowboy_req:set_resp_body({sendfile, 0, filelib:file_size(AppFile), AppFile}, Req0); + _ -> + cowboy_req:set_resp_body(<<"OK">>, Req0) + end, + case Arg of + <<"override">> -> + cowboy_req:reply(200, #{}, <<"OVERRIDE">>, Req); + _ -> + cowboy_req:reply(200, Req) + end, + {ok, Req, Opts}; +do(<<"has_resp_header">>, Req0, Opts) -> + false = cowboy_req:has_resp_header(<<"content-type">>, Req0), + Req = cowboy_req:set_resp_header(<<"content-type">>, <<"text/plain">>, Req0), + true = cowboy_req:has_resp_header(<<"content-type">>, Req), + cowboy_req:reply(200, #{}, "OK", Req), + {ok, Req, Opts}; +do(<<"has_resp_body">>, Req0, Opts) -> + case cowboy_req:binding(arg, Req0) of + <<"sendfile">> -> + %% @todo Cases for sendfile. Note that sendfile 0 is unallowed. + false = cowboy_req:has_resp_body(Req0), + Req = cowboy_req:set_resp_body({sendfile, 0, 10, code:where_is_file("cowboy.app")}, Req0), + true = cowboy_req:has_resp_body(Req), + cowboy_req:reply(200, #{}, <<"OK">>, Req), + {ok, Req, Opts}; + undefined -> + false = cowboy_req:has_resp_body(Req0), + Req = cowboy_req:set_resp_body(<<"OK">>, Req0), + true = cowboy_req:has_resp_body(Req), + cowboy_req:reply(200, #{}, Req), + {ok, Req, Opts} + end; +do(<<"delete_resp_header">>, Req0, Opts) -> + false = cowboy_req:has_resp_header(<<"content-type">>, Req0), + Req1 = cowboy_req:set_resp_header(<<"content-type">>, <<"text/plain">>, Req0), + true = cowboy_req:has_resp_header(<<"content-type">>, Req1), + Req = cowboy_req:delete_resp_header(<<"content-type">>, Req1), + false = cowboy_req:has_resp_header(<<"content-type">>, Req), + cowboy_req:reply(200, #{}, "OK", Req), + {ok, Req, Opts}; +do(<<"reply2">>, Req, Opts) -> + case cowboy_req:binding(arg, Req) of + <<"binary">> -> + cowboy_req:reply(<<"200 GOOD">>, Req); + <<"error">> -> + ct_helper:ignore(cowboy_req, reply, 4), + cowboy_req:reply(ok, Req); + <<"twice">> -> + cowboy_req:reply(200, Req), + cowboy_req:reply(200, Req); + Status -> + cowboy_req:reply(binary_to_integer(Status), Req) + end, + {ok, Req, Opts}; +do(<<"reply3">>, Req, Opts) -> + case cowboy_req:binding(arg, Req) of + <<"error">> -> + ct_helper:ignore(cowboy_req, reply, 4), + cowboy_req:reply(200, ok, Req); + Status -> + cowboy_req:reply(binary_to_integer(Status), + #{<<"content-type">> => <<"text/plain">>}, Req) + end, + {ok, Req, Opts}; +do(<<"reply4">>, Req, Opts) -> + case cowboy_req:binding(arg, Req) of + <<"error">> -> + ct_helper:ignore(erlang, iolist_size, 1), + cowboy_req:reply(200, #{}, ok, Req); + Status -> + cowboy_req:reply(binary_to_integer(Status), #{}, <<"OK">>, Req) + end, + {ok, Req, Opts}; +do(<<"stream_reply2">>, Req, Opts) -> + case cowboy_req:binding(arg, Req) of + <<"binary">> -> + cowboy_req:stream_reply(<<"200 GOOD">>, Req); + <<"error">> -> + ct_helper:ignore(cowboy_req, stream_reply, 3), + cowboy_req:stream_reply(ok, Req); + Status -> + cowboy_req:stream_reply(binary_to_integer(Status), Req) + end, + stream_body(Req), + {ok, Req, Opts}; +do(<<"stream_reply3">>, Req, Opts) -> + case cowboy_req:binding(arg, Req) of + <<"error">> -> + ct_helper:ignore(cowboy_req, stream_reply, 3), + cowboy_req:stream_reply(200, ok, Req); + Status -> + cowboy_req:stream_reply(binary_to_integer(Status), + #{<<"content-type">> => <<"text/plain">>}, Req) + end, + stream_body(Req), + {ok, Req, Opts}; +do(<<"stream_body">>, Req, Opts) -> + %% Call stream_body without initiating streaming. + cowboy_req:stream_body(<<0:800000>>, fin, Req), + {ok, Req, Opts}; +do(<<"push">>, Req, Opts) -> + case cowboy_req:binding(arg, Req) of + <<"method">> -> + cowboy_req:push("/static/style.css", #{<<"accept">> => <<"text/css">>}, Req, + #{method => <<"HEAD">>}); + <<"origin">> -> + cowboy_req:push("/static/style.css", #{<<"accept">> => <<"text/css">>}, Req, + #{scheme => <<"ftp">>, host => <<"127.0.0.1">>, port => 21}); + <<"qs">> -> + cowboy_req:push("/static/style.css", #{<<"accept">> => <<"text/css">>}, Req, + #{qs => <<"server=cowboy&version=2.0">>}); + _ -> + cowboy_req:push("/static/style.css", #{<<"accept">> => <<"text/css">>}, Req), + %% The text/plain mime is not defined by default, so a 406 will be returned. + cowboy_req:push("/static/plain.txt", #{<<"accept">> => <<"text/plain">>}, Req) + end, + cowboy_req:reply(200, Req), + {ok, Req, Opts}. + +stream_body(Req) -> + _ = [cowboy_req:stream_body(<<0:800000>>, nofin, Req) || _ <- lists:seq(1,9)], + cowboy_req:stream_body(<<0:800000>>, fin, Req). diff --git a/test/http_SUITE_data/http_body_qs.erl b/test/http_SUITE_data/http_body_qs.erl index e0673cf..09ca5e4 100644 --- a/test/http_SUITE_data/http_body_qs.erl +++ b/test/http_SUITE_data/http_body_qs.erl @@ -10,7 +10,7 @@ init(Req, Opts) -> {ok, maybe_echo(Method, HasBody, Req), Opts}. maybe_echo(<<"POST">>, true, Req) -> - case cowboy_req:body_qs(Req) of + case cowboy_req:read_urlencoded_body(Req) of {badlength, Req2} -> echo(badlength, Req2); {ok, PostVals, Req2} -> diff --git a/test/http_SUITE_data/http_loop_stream_recv.erl b/test/http_SUITE_data/http_loop_stream_recv.erl index c006b6d..18b3d29 100644 --- a/test/http_SUITE_data/http_loop_stream_recv.erl +++ b/test/http_SUITE_data/http_loop_stream_recv.erl @@ -15,7 +15,7 @@ info(stream, Req, undefined) -> stream(Req, 1, <<>>). stream(Req, ID, Acc) -> - case cowboy_req:body(Req) of + case cowboy_req:read_body(Req) of {ok, <<>>, Req2} -> {stop, cowboy_req:reply(200, Req2), undefined}; {_, Data, Req2} -> diff --git a/test/req_SUITE.erl b/test/req_SUITE.erl index 648ebcd..b0aabad 100644 --- a/test/req_SUITE.erl +++ b/test/req_SUITE.erl @@ -34,6 +34,13 @@ groups() -> %% @todo With compression enabled. ]. +init_per_suite(Config) -> + ct_helper:create_static_dir(config(priv_dir, Config) ++ "/static"), + Config. + +end_per_suite(Config) -> + ct_helper:delete_static_dir(config(priv_dir, Config) ++ "/static"). + init_per_group(Name, Config) -> cowboy_test:init_common_groups(Name, Config, ?MODULE). @@ -42,10 +49,20 @@ end_per_group(Name, _) -> %% Routes. -init_dispatch(_) -> +init_dispatch(Config) -> cowboy_router:compile([{"[...]", [ - {"/no/:key", echo_h, []}, + {"/static/[...]", cowboy_static, {dir, config(priv_dir, Config) ++ "/static"}}, + %% @todo Seriously InitialState should be optional. + {"/resp/:key[/:arg]", resp_h, []}, + {"/multipart[/:key]", multipart_h, []}, {"/args/:key/:arg[/:default]", echo_h, []}, + {"/crash/:key/period", echo_h, #{length => infinity, period => 1000, crash => true}}, + {"/no-opts/:key", echo_h, #{crash => true}}, + {"/opts/:key/length", echo_h, #{length => 1000}}, + {"/opts/:key/period", echo_h, #{length => infinity, period => 1000}}, + {"/opts/:key/timeout", echo_h, #{timeout => 1000, crash => true}}, + {"/full/:key", echo_h, []}, + {"/no/:key", echo_h, []}, {"/:key/[...]", echo_h, []} ]}]). @@ -55,15 +72,32 @@ do_body(Method, Path, Config) -> do_body(Method, Path, [], Config). do_body(Method, Path, Headers, Config) -> + do_body(Method, Path, Headers, <<>>, Config). + +do_body(Method, Path, Headers, Body, Config) -> ConnPid = gun_open(Config), - Ref = gun:request(ConnPid, Method, Path, Headers), + Ref = case Body of + <<>> -> gun:request(ConnPid, Method, Path, Headers); + _ -> gun:request(ConnPid, Method, Path, Headers, Body) + end, {response, IsFin, 200, _} = gun:await(ConnPid, Ref), - {ok, Body} = case IsFin of + {ok, RespBody} = case IsFin of nofin -> gun:await_body(ConnPid, Ref); fin -> {ok, <<>>} end, gun:close(ConnPid), - Body. + RespBody. + +do_get(Path, Config) -> + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, Path, []), + {response, IsFin, Status, Headers} = gun:await(ConnPid, Ref), + {ok, RespBody} = case IsFin of + nofin -> gun:await_body(ConnPid, Ref); + fin -> {ok, <<>>} + end, + gun:close(ConnPid), + {Status, Headers, RespBody}. do_get_body(Path, Config) -> do_get_body(Path, [], Config). @@ -71,7 +105,7 @@ do_get_body(Path, Config) -> do_get_body(Path, Headers, Config) -> do_body("GET", Path, Headers, Config). -%% Tests. +%% Tests: Request. binding(Config) -> doc("Value bound from request URI path with/without default."), @@ -109,6 +143,7 @@ host_info(Config) -> <<"[<<\"localhost\">>]">> = do_get_body("/host_info", Config), ok. +%% @todo Actually write the related unit tests. match_cookies(Config) -> doc("Matched request cookies."), <<"#{}">> = do_get_body("/match/cookies", [{<<"cookie">>, "a=b; c=d"}], Config), @@ -119,6 +154,7 @@ match_cookies(Config) -> %% This function is tested more extensively through unit tests. ok. +%% @todo Actually write the related unit tests. match_qs(Config) -> doc("Matched request URI query string."), <<"#{}">> = do_get_body("/match/qs?a=b&c=d", Config), @@ -131,7 +167,7 @@ match_qs(Config) -> method(Config) -> doc("Request method."), <<"GET">> = do_body("GET", "/method", Config), - <<"HEAD">> = do_body("HEAD", "/method", Config), + <<>> = do_body("HEAD", "/method", Config), <<"OPTIONS">> = do_body("OPTIONS", "/method", Config), <<"PATCH">> = do_body("PATCH", "/method", Config), <<"POST">> = do_body("POST", "/method", Config), @@ -147,6 +183,9 @@ parse_cookies(Config) -> = do_get_body("/parse_cookies", [{<<"cookie">>, "cake=strawberry"}], Config), <<"[{<<\"cake\">>,<<\"strawberry\">>},{<<\"color\">>,<<\"blue\">>}]">> = do_get_body("/parse_cookies", [{<<"cookie">>, "cake=strawberry; color=blue"}], Config), + <<"[{<<\"cake\">>,<<\"strawberry\">>},{<<\"color\">>,<<\"blue\">>}]">> + = do_get_body("/parse_cookies", + [{<<"cookie">>, "cake=strawberry"}, {<<"cookie">>, "color=blue"}], Config), ok. parse_header(Config) -> @@ -252,3 +291,394 @@ version(Config) -> <<"HTTP/1.1">> when Protocol =:= http -> ok; <<"HTTP/2">> when Protocol =:= http2 -> ok end. + +%% Tests: Request body. + +body_length(Config) -> + doc("Request body length."), + <<"0">> = do_get_body("/body_length", Config), + <<"12">> = do_body("POST", "/body_length", [], "hello world!", Config), + ok. + +has_body(Config) -> + doc("Has a request body?"), + <<"false">> = do_get_body("/has_body", Config), + <<"true">> = do_body("POST", "/has_body", [], "hello world!", Config), + ok. + +read_body(Config) -> + doc("Request body."), + <<>> = do_get_body("/read_body", Config), + <<"hello world!">> = do_body("POST", "/read_body", [], "hello world!", Config), + %% We expect to have read *at least* 1000 bytes. + <<0:8000, _/bits>> = do_body("POST", "/opts/read_body/length", [], <<0:8000000>>, Config), + %% We read any length for at most 1 second. + %% + %% The body is sent twice, first with nofin, then wait 2 seconds, then again with fin. + <<0:8000000>> = do_read_body_period("/opts/read_body/period", <<0:8000000>>, Config), + %% The timeout value is set too low on purpose to ensure a crash occurs. + ok = do_read_body_timeout("/opts/read_body/timeout", <<0:8000000>>, Config), + %% 10MB body larger than default length. + <<0:80000000>> = do_body("POST", "/full/read_body", [], <<0:80000000>>, Config), + ok. + +do_read_body_period(Path, Body, Config) -> + ConnPid = gun_open(Config), + Ref = gun:request(ConnPid, "POST", Path, [ + {<<"content-length">>, integer_to_binary(byte_size(Body) * 2)} + ]), + gun:data(ConnPid, Ref, nofin, Body), + timer:sleep(2000), + gun:data(ConnPid, Ref, fin, Body), + {response, nofin, 200, _} = gun:await(ConnPid, Ref), + {ok, RespBody} = gun:await_body(ConnPid, Ref), + gun:close(ConnPid), + RespBody. + +%% We expect a crash. +do_read_body_timeout(Path, Body, Config) -> + ConnPid = gun_open(Config), + Ref = gun:request(ConnPid, "POST", Path, [ + {<<"content-length">>, integer_to_binary(byte_size(Body))} + ]), + {response, _, 500, _} = gun:await(ConnPid, Ref), + gun:close(ConnPid). + +%% @todo Do we really want a key/value list here instead of a map? +read_urlencoded_body(Config) -> + doc("application/x-www-form-urlencoded request body."), + <<"[]">> = do_body("POST", "/read_urlencoded_body", [], <<>>, Config), + <<"[{<<\"abc\">>,true}]">> = do_body("POST", "/read_urlencoded_body", [], "abc", Config), + <<"[{<<\"a\">>,<<\"b\">>},{<<\"c\">>,<<\"d e\">>}]">> + = do_body("POST", "/read_urlencoded_body", [], "a=b&c=d+e", Config), + %% Send a 10MB body, larger than the default length, to ensure a crash occurs. + ok = do_read_urlencoded_body_too_large("/no-opts/read_urlencoded_body", + string:chars($a, 10000000), Config), + %% We read any length for at most 1 second. + %% + %% The body is sent twice, first with nofin, then wait 1.1 second, then again with fin. + %% We expect the handler to crash because read_urlencoded_body expects the full body. + ok = do_read_urlencoded_body_too_long("/crash/read_urlencoded_body/period", <<"abc">>, Config), + %% The timeout value is set too low on purpose to ensure a crash occurs. + ok = do_read_body_timeout("/opts/read_urlencoded_body/timeout", <<"abc">>, Config), + ok. + +%% We expect a crash. +do_read_urlencoded_body_too_large(Path, Body, Config) -> + ConnPid = gun_open(Config), + Ref = gun:request(ConnPid, "POST", Path, [ + {<<"content-length">>, integer_to_binary(iolist_size(Body))} + ]), + gun:data(ConnPid, Ref, fin, Body), + {response, _, 500, _} = gun:await(ConnPid, Ref), + gun:close(ConnPid). + +%% We expect a crash. +do_read_urlencoded_body_too_long(Path, Body, Config) -> + ConnPid = gun_open(Config), + Ref = gun:request(ConnPid, "POST", Path, [ + {<<"content-length">>, integer_to_binary(byte_size(Body) * 2)} + ]), + gun:data(ConnPid, Ref, nofin, Body), + timer:sleep(1100), + gun:data(ConnPid, Ref, fin, Body), + {response, _, 500, _} = gun:await(ConnPid, Ref), + gun:close(ConnPid). + +multipart(Config) -> + doc("Multipart request body."), + do_multipart("/multipart", Config). + +do_multipart(Path, Config) -> + LargeBody = iolist_to_binary(string:chars($a, 10000000)), + ReqBody = [ + "--deadbeef\r\nContent-Type: text/plain\r\n\r\nCowboy is an HTTP server.\r\n" + "--deadbeef\r\nContent-Type: application/octet-stream\r\nX-Custom: value\r\n\r\n", LargeBody, "\r\n" + "--deadbeef--" + ], + RespBody = do_body("POST", Path, [ + {<<"content-type">>, <<"multipart/mixed; boundary=deadbeef">>} + ], ReqBody, Config), + [ + {[{<<"content-type">>, <<"text/plain">>}], <<"Cowboy is an HTTP server.">>}, + {LargeHeaders, LargeBody} + ] = binary_to_term(RespBody), + %% @todo Multipart header order is currently undefined. + [ + {<<"content-type">>, <<"application/octet-stream">>}, + {<<"x-custom">>, <<"value">>} + ] = lists:sort(LargeHeaders), + ok. + +read_part_skip_body(Config) -> + doc("Multipart request body skipping part bodies."), + LargeBody = iolist_to_binary(string:chars($a, 10000000)), + ReqBody = [ + "--deadbeef\r\nContent-Type: text/plain\r\n\r\nCowboy is an HTTP server.\r\n" + "--deadbeef\r\nContent-Type: application/octet-stream\r\nX-Custom: value\r\n\r\n", LargeBody, "\r\n" + "--deadbeef--" + ], + RespBody = do_body("POST", "/multipart/skip_body", [ + {<<"content-type">>, <<"multipart/mixed; boundary=deadbeef">>} + ], ReqBody, Config), + [ + [{<<"content-type">>, <<"text/plain">>}], + LargeHeaders + ] = binary_to_term(RespBody), + %% @todo Multipart header order is currently undefined. + [ + {<<"content-type">>, <<"application/octet-stream">>}, + {<<"x-custom">>, <<"value">>} + ] = lists:sort(LargeHeaders), + ok. + +%% @todo When reading a multipart body, length and period +%% only apply to a single read_body call. We may want a +%% separate option to know how many reads we want to do +%% before we give up. + +read_part2(Config) -> + doc("Multipart request body using read_part/2."), + %% Override the length and period values only, making + %% the request process use more read_body calls. + %% + %% We do not try a custom timeout value since this would + %% be the same test as read_body/2. + do_multipart("/multipart/read_part2", Config). + +read_part_body2(Config) -> + doc("Multipart request body using read_part_body/2."), + %% Override the length and period values only, making + %% the request process use more read_body calls. + %% + %% We do not try a custom timeout value since this would + %% be the same test as read_body/2. + do_multipart("/multipart/read_part_body2", Config). + +%% Tests: Response. + +%% @todo We want to crash when calling set_resp_* or related +%% functions after the reply has been sent. + +set_resp_cookie(Config) -> + doc("Response using set_resp_cookie."), + %% Single cookie, no options. + {200, Headers1, _} = do_get("/resp/set_resp_cookie3", Config), + {_, <<"mycookie=myvalue; Version=1">>} + = lists:keyfind(<<"set-cookie">>, 1, Headers1), + %% Single cookie, with options. + {200, Headers2, _} = do_get("/resp/set_resp_cookie4", Config), + {_, <<"mycookie=myvalue; Version=1; Path=/resp/set_resp_cookie4">>} + = lists:keyfind(<<"set-cookie">>, 1, Headers2), + %% Multiple cookies. + {200, Headers3, _} = do_get("/resp/set_resp_cookie3/multiple", Config), + [_, _] = [H || H={<<"set-cookie">>, _} <- Headers3], + %% Overwrite previously set cookie. + {200, Headers4, _} = do_get("/resp/set_resp_cookie3/overwrite", Config), + {_, <<"mycookie=overwrite; Version=1">>} + = lists:keyfind(<<"set-cookie">>, 1, Headers4), + ok. + +set_resp_header(Config) -> + doc("Response using set_resp_header."), + {200, Headers, <<"OK">>} = do_get("/resp/set_resp_header", Config), + true = lists:keymember(<<"content-type">>, 1, Headers), + ok. + +set_resp_body(Config) -> + doc("Response using set_resp_body."), + {200, _, <<"OK">>} = do_get("/resp/set_resp_body", Config), + {200, _, <<"OVERRIDE">>} = do_get("/resp/set_resp_body/override", Config), + {ok, AppFile} = file:read_file(code:where_is_file("cowboy.app")), + {200, _, AppFile} = do_get("/resp/set_resp_body/sendfile", Config), + ok. + +has_resp_header(Config) -> + doc("Has response header?"), + {200, Headers, <<"OK">>} = do_get("/resp/has_resp_header", Config), + true = lists:keymember(<<"content-type">>, 1, Headers), + ok. + +has_resp_body(Config) -> + doc("Has response body?"), + {200, _, <<"OK">>} = do_get("/resp/has_resp_body", Config), + {200, _, <<"OK">>} = do_get("/resp/has_resp_body/sendfile", Config), + ok. + +delete_resp_header(Config) -> + doc("Delete response header."), + {200, Headers, <<"OK">>} = do_get("/resp/delete_resp_header", Config), + false = lists:keymember(<<"content-type">>, 1, Headers), + ok. + +reply2(Config) -> + doc("Response with default headers and no body."), + {200, _, _} = do_get("/resp/reply2/200", Config), + {201, _, _} = do_get("/resp/reply2/201", Config), + {404, _, _} = do_get("/resp/reply2/404", Config), + {200, _, _} = do_get("/resp/reply2/binary", Config), + {500, _, _} = do_get("/resp/reply2/error", Config), + %% @todo We want to crash when reply or stream_reply is called twice. + %% How to test this properly? This isn't enough. + {200, _, _} = do_get("/resp/reply2/twice", Config), + ok. + +reply3(Config) -> + doc("Response with additional headers and no body."), + {200, Headers1, _} = do_get("/resp/reply3/200", Config), + true = lists:keymember(<<"content-type">>, 1, Headers1), + {201, Headers2, _} = do_get("/resp/reply3/201", Config), + true = lists:keymember(<<"content-type">>, 1, Headers2), + {404, Headers3, _} = do_get("/resp/reply3/404", Config), + true = lists:keymember(<<"content-type">>, 1, Headers3), + {500, _, _} = do_get("/resp/reply3/error", Config), + ok. + +reply4(Config) -> + doc("Response with additional headers and body."), + {200, _, <<"OK">>} = do_get("/resp/reply4/200", Config), + {201, _, <<"OK">>} = do_get("/resp/reply4/201", Config), + {404, _, <<"OK">>} = do_get("/resp/reply4/404", Config), + {500, _, _} = do_get("/resp/reply4/error", Config), + ok. + +%% @todo Crash when stream_reply is called twice. + +stream_reply2(Config) -> + doc("Response with default headers and streamed body."), + Body = <<0:8000000>>, + {200, _, Body} = do_get("/resp/stream_reply2/200", Config), + {201, _, Body} = do_get("/resp/stream_reply2/201", Config), + {404, _, Body} = do_get("/resp/stream_reply2/404", Config), + {200, _, Body} = do_get("/resp/stream_reply2/binary", Config), + {500, _, _} = do_get("/resp/stream_reply2/error", Config), + ok. + +stream_reply3(Config) -> + doc("Response with additional headers and streamed body."), + Body = <<0:8000000>>, + {200, Headers1, Body} = do_get("/resp/stream_reply3/200", Config), + true = lists:keymember(<<"content-type">>, 1, Headers1), + {201, Headers2, Body} = do_get("/resp/stream_reply3/201", Config), + true = lists:keymember(<<"content-type">>, 1, Headers2), + {404, Headers3, Body} = do_get("/resp/stream_reply3/404", Config), + true = lists:keymember(<<"content-type">>, 1, Headers3), + {500, _, _} = do_get("/resp/stream_reply3/error", Config), + ok. + +%% @todo Crash when calling stream_body after the fin flag has been set. +%% @todo Crash when calling stream_body after calling reply. +%% @todo Crash when calling stream_body before calling stream_reply. + +%% Tests: Push. + +%% @todo We want to crash when push is called after reply has been initiated. + +push(Config) -> + case config(protocol, Config) of + http -> do_push_http("/resp/push", Config); + http2 -> do_push_http2(Config) + end. + +push_method(Config) -> + case config(protocol, Config) of + http -> do_push_http("/resp/push/method", Config); + http2 -> do_push_http2_method(Config) + end. + + +push_origin(Config) -> + case config(protocol, Config) of + http -> do_push_http("/resp/push/origin", Config); + http2 -> do_push_http2_origin(Config) + end. + +push_qs(Config) -> + case config(protocol, Config) of + http -> do_push_http("/resp/push/qs", Config); + http2 -> do_push_http2_qs(Config) + end. + +do_push_http(Path, Config) -> + doc("Ignore pushed responses when protocol is HTTP/1.1."), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, Path, []), + {response, fin, 200, _} = gun:await(ConnPid, Ref), + ok. + +do_push_http2(Config) -> + doc("Pushed responses."), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/resp/push", []), + %% We expect two pushed resources. + Origin = iolist_to_binary([ + case config(type, Config) of + tcp -> "http"; + ssl -> "https" + end, + "://localhost:", + integer_to_binary(config(port, Config)) + ]), + OriginLen = byte_size(Origin), + {push, PushCSS, <<"GET">>, <<Origin:OriginLen/binary, "/static/style.css">>, + [{<<"accept">>,<<"text/css">>}]} = gun:await(ConnPid, Ref), + {push, PushTXT, <<"GET">>, <<Origin:OriginLen/binary, "/static/plain.txt">>, + [{<<"accept">>,<<"text/plain">>}]} = gun:await(ConnPid, Ref), + %% Pushed CSS. + {response, nofin, 200, HeadersCSS} = gun:await(ConnPid, PushCSS), + {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, HeadersCSS), + {ok, <<"body{color:red}\n">>} = gun:await_body(ConnPid, PushCSS), + %% Pushed TXT is 406 because the pushed accept header uses an undefined type. + {response, fin, 406, _} = gun:await(ConnPid, PushTXT), + %% Let's not forget about the response to the client's request. + {response, fin, 200, _} = gun:await(ConnPid, Ref), + gun:close(ConnPid). + +do_push_http2_method(Config) -> + doc("Pushed response with non-GET method."), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/resp/push/method", []), + %% Pushed CSS. + {push, PushCSS, <<"HEAD">>, _, [{<<"accept">>,<<"text/css">>}]} = gun:await(ConnPid, Ref), + {response, fin, 200, HeadersCSS} = gun:await(ConnPid, PushCSS), + {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, HeadersCSS), + %% Let's not forget about the response to the client's request. + {response, fin, 200, _} = gun:await(ConnPid, Ref), + gun:close(ConnPid). + +do_push_http2_origin(Config) -> + doc("Pushed response with custom scheme/host/port."), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/resp/push/origin", []), + %% Pushed CSS. + {push, PushCSS, <<"GET">>, <<"ftp://127.0.0.1:21/static/style.css">>, + [{<<"accept">>,<<"text/css">>}]} = gun:await(ConnPid, Ref), + {response, nofin, 200, HeadersCSS} = gun:await(ConnPid, PushCSS), + {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, HeadersCSS), + {ok, <<"body{color:red}\n">>} = gun:await_body(ConnPid, PushCSS), + %% Let's not forget about the response to the client's request. + {response, fin, 200, _} = gun:await(ConnPid, Ref), + gun:close(ConnPid). + +do_push_http2_qs(Config) -> + doc("Pushed response with query string."), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/resp/push/qs", []), + %% Pushed CSS. + Origin = iolist_to_binary([ + case config(type, Config) of + tcp -> "http"; + ssl -> "https" + end, + "://localhost:", + integer_to_binary(config(port, Config)) + ]), + OriginLen = byte_size(Origin), + {push, PushCSS, <<"GET">>, <<Origin:OriginLen/binary, "/static/style.css?server=cowboy&version=2.0">>, + [{<<"accept">>,<<"text/css">>}]} = gun:await(ConnPid, Ref), + {response, nofin, 200, HeadersCSS} = gun:await(ConnPid, PushCSS), + {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, HeadersCSS), + {ok, <<"body{color:red}\n">>} = gun:await_body(ConnPid, PushCSS), + %% Let's not forget about the response to the client's request. + {response, fin, 200, _} = gun:await(ConnPid, Ref), + gun:close(ConnPid). |