diff --git a/Makefile b/Makefile index b471713..4db2e67 100644 --- a/Makefile +++ b/Makefile @@ -2,4 +2,4 @@ compile: ; rebar3 do compile, xref eunit: ; rebar3 eunit init_dialyzer: ; rebar3 dialyzer -s false dialyzer: ; rebar3 dialyzer -u false -travis: ; rebar3 do xref, dialyzer, eunit +travis: ; rebar3 do xref, dialyzer, eunit && rebar3 coveralls send diff --git a/README.md b/README.md index 2a8fec5..baab683 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # elli - Erlang web server for HTTP APIs -[![Travis CI][travis badge]][travis builds] [![Hex.pm][hex badge]][hex package] -[![Erlang][erlang badge]][erlang downloads] [![Documentation][doc badge]][docs] +[![Erlang][erlang badge]][erlang downloads] +[![Travis CI][travis badge]][travis builds] +[![Coverage Status][coveralls badge]][coveralls link] [![MIT License][license badge]](LICENSE) [travis builds]: https://travis-ci.org/elli-lib/elli @@ -15,6 +16,8 @@ [erlang downloads]: http://www.erlang.org/downloads [doc badge]: https://img.shields.io/badge/docs-edown-green.svg [docs]: doc/README.md +[coveralls badge]: https://coveralls.io/repos/github/elli-lib/elli/badge.svg?branch=develop +[coveralls link]: https://coveralls.io/github/elli-lib/elli?branch=develop [license badge]: https://img.shields.io/badge/license-MIT-blue.svg Elli is a webserver you can run inside your Erlang application to diff --git a/doc/README.md b/doc/README.md index 1a00896..05af46e 100644 --- a/doc/README.md +++ b/doc/README.md @@ -159,6 +159,7 @@ about benchmarking HTTP servers. + diff --git a/doc/edoc-info b/doc/edoc-info index 7dfc7ca..98f05d9 100644 --- a/doc/edoc-info +++ b/doc/edoc-info @@ -1,5 +1,5 @@ %% encoding: UTF-8 {application,elli}. -{modules,[elli,elli_example_callback,elli_example_callback_handover, +{modules,[adder,elli,elli_example_callback,elli_example_callback_handover, elli_handler,elli_http,elli_middleware,elli_middleware_compress, elli_request,elli_tcp,elli_test,elli_util]}. diff --git a/doc/elli_handler.md b/doc/elli_handler.md index 2662af9..5ba6bc5 100644 --- a/doc/elli_handler.md +++ b/doc/elli_handler.md @@ -53,6 +53,6 @@ See [`elli_example_callback:handle_event/3`](elli_example_callback.md#handle_eve

-result() = {elli:response_code() | ok, elli:body()} | {elli:response_code() | ok, elli:headers(), elli:body()} | ignore
+result() = {elli:response_code() | ok, elli:headers(), {file, file:name_all()} | {file, file:name_all(), elli_util:range()}} | {elli:response_code() | ok, elli:headers(), elli:body()} | {elli:response_code() | ok, elli:body()} | {chunk, elli:headers()} | {chunk, elli:headers(), elli:body()} | ignore
 
diff --git a/doc/elli_test.md b/doc/elli_test.md index aa893f7..12f4df9 100644 --- a/doc/elli_test.md +++ b/doc/elli_test.md @@ -32,7 +32,7 @@ The unit tests below test `elli_example_callback`. ### call/5 ###

-call(Method, Path, Headers, Body, Opts) -> elli:req()
+call(Method, Path, Headers, Body, Opts) -> elli_handler:result()
 
diff --git a/doc/elli_util.md b/doc/elli_util.md index e4ce653..de10432 100644 --- a/doc/elli_util.md +++ b/doc/elli_util.md @@ -49,10 +49,10 @@ Encode Range to a Content-Range value. ### file_size/1 ###

-file_size(Filename::file:name()) -> non_neg_integer() | {error, Reason}
+file_size(Filename) -> Size | {error, Reason}
 
- + Get the size in bytes of the file. diff --git a/rebar.config b/rebar.config index 88e6072..949de8a 100644 --- a/rebar.config +++ b/rebar.config @@ -1,6 +1,5 @@ {erl_first_files, ["src/elli_handler.erl"]}. {erl_opts, [debug_info, {i, "include"}]}. -{cover_enabled, true}. {deps, []}. {xref_checks, [undefined_function_calls,locals_not_used]}. {profiles, [ @@ -12,7 +11,25 @@ ]}, {doclet, edown_doclet} ]} + ]}, + {test, [ + {deps, [{hackney, "1.6.3"}]} ]} ]}. {shell, [{script_file, "bin/shell.escript"}]}. + +{project_plugins, [ + {coveralls, "1.3.0"}, + {rebar3_lint, "0.1.7"} +]}. + +{provider_hooks, [{pre, [{eunit, lint}]}]}. + +{cover_enabled, true}. +{cover_export_enabled, true}. +{cover_excl_mods, [ + elli_handler +]}. +{coveralls_coverdata, "_build/test/cover/eunit.coverdata"}. +{coveralls_service_name, "travis-ci"}. diff --git a/rebar.config.script b/rebar.config.script index 03e3581..1411e37 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -1,12 +1,7 @@ -case erl_internal:bif(is_map, 1) of - false -> CONFIG; - true -> - Lint = {rebar3_lint, - {git, "git://github.com/project-fifo/rebar3_lint.git", - {tag, "0.1.6"}}}, - Config1 = lists:keystore(project_plugins, 1, CONFIG, - {project_plugins, [Lint]}), - Config2 = lists:keystore(provider_hooks, 1, Config1, - {provider_hooks, [{pre, [{eunit, lint}]}]}) - +case os:getenv("TRAVIS") of + "true" -> + JobId = os:getenv("TRAVIS_JOB_ID"), + lists:keystore(coveralls_service_job_id, 1, CONFIG, + {coveralls_service_job_id, JobId}); + _ -> CONFIG end. diff --git a/src/elli.app.src b/src/elli.app.src index 7b54789..4786b15 100644 --- a/src/elli.app.src +++ b/src/elli.app.src @@ -1,7 +1,7 @@ {application, elli, [ {description, "Erlang web server for HTTP APIs"}, - {vsn, "2.0.0"}, + {vsn, "2.0.1"}, {modules, [ elli, elli_example_callback, @@ -24,7 +24,7 @@ ]}, {env, []}, - {maintainers,["Eric Bailey", "Knut Nesheim", "Tristan Sloughter"]}, - {licenses,["MIT"]}, - {links,[{"Github","https://github.com/elli-lib/elli"}]} + {maintainers, ["Eric Bailey", "Knut Nesheim", "Tristan Sloughter"]}, + {licenses, ["MIT"]}, + {links, [{"GitHub", "https://github.com/elli-lib/elli"}]} ]}. diff --git a/src/elli_example_callback.erl b/src/elli_example_callback.erl index f0333e6..6677064 100644 --- a/src/elli_example_callback.erl +++ b/src/elli_example_callback.erl @@ -181,6 +181,9 @@ handle('GET', [<<"chunked">>], Req) -> handle('GET', [<<"shorthand">>], _Req) -> {200, <<"hello">>}; +handle('GET', [<<"ip">>], Req) -> + {<<"200 OK">>, elli_request:peer(Req)}; + handle('GET', [<<"304">>], _Req) -> %% A "Not Modified" response is exactly like a normal response (so %% Content-Length is included), but the body will not be sent. diff --git a/src/elli_handler.erl b/src/elli_handler.erl index 6d2de3c..9342459 100644 --- a/src/elli_handler.erl +++ b/src/elli_handler.erl @@ -22,8 +22,14 @@ | client_closed | client_timeout | invalid_return. --type result() :: {elli:response_code() | ok, elli:body()} +-type result() :: {elli:response_code() | ok, + elli:headers(), + {file, file:name_all()} + | {file, file:name_all(), elli_util:range()}} | {elli:response_code() | ok, elli:headers(), elli:body()} + | {elli:response_code() | ok, elli:body()} + | {chunk, elli:headers()} + | {chunk, elli:headers(), elli:body()} | ignore. -callback handle(Req :: elli:req(), callback_args()) -> result(). diff --git a/src/elli_http.erl b/src/elli_http.erl index 8b5caca..a58c1f1 100644 --- a/src/elli_http.erl +++ b/src/elli_http.erl @@ -70,7 +70,7 @@ keepalive_loop(Socket, Options, Callback) -> keepalive_loop(Socket, NumRequests, Buffer, Options, Callback) -> case ?MODULE:handle_request(Socket, Buffer, Options, Callback) of {keep_alive, NewBuffer} -> - ?MODULE:keepalive_loop(Socket, NumRequests, + ?MODULE:keepalive_loop(Socket, NumRequests + 1, NewBuffer, Options, Callback); {close, _} -> elli_tcp:close(Socket), @@ -87,7 +87,6 @@ keepalive_loop(Socket, NumRequests, Buffer, Options, Callback) -> Callback :: elli_handler:callback(), ConnToken :: {'keep_alive' | 'close', binary()}. handle_request(S, PrevB, Opts, {Mod, Args} = Callback) -> - t(request_start), {Method, RawPath, V, B0} = get_request(S, PrevB, Opts, Callback), t(headers_start), {RequestHeaders, B1} = get_headers(S, V, B0, Opts, Callback), @@ -280,10 +279,7 @@ send_server_error(Socket) -> send_rescue_response(Socket, 500, <<"Server Error">>). send_rescue_response(Socket, Code, Body) -> - Response = [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>, - <<"Content-Length: ">>, integer_to_list(size(Body)), <<"\r\n">>, - <<"\r\n">>, - Body], + Response = http_response(Code, Body), elli_tcp:send(Socket, Response). %% @doc Execute the user callback, translating failure into a proper response. @@ -407,23 +403,17 @@ send_chunk(Socket, Data) -> %% %% @doc Retrieve the request line. -get_request(Socket, Buffer, Options, {Mod, Args} = Callback) -> +get_request(Socket, <<>>, Options, Callback) -> + NewBuffer = recv_request(Socket, <<>>, Options, Callback), + get_request(Socket, NewBuffer, Options, Callback); +get_request(Socket, Buffer, Options, Callback) -> + t(request_start), + get_request_(Socket, Buffer, Options, Callback). + +get_request_(Socket, Buffer, Options, {Mod, Args} = Callback) -> case erlang:decode_packet(http_bin, Buffer, []) of {more, _} -> - case elli_tcp:recv(Socket, 0, request_timeout(Options)) of - {ok, Data} -> - NewBuffer = <>, - get_request(Socket, NewBuffer, Options, Callback); - {error, timeout} -> - handle_event(Mod, request_timeout, [], Args), - elli_tcp:close(Socket), - exit(normal); - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> - handle_event(Mod, request_closed, [], Args), - elli_tcp:close(Socket), - exit(normal) - end; + recv_request(Socket, Buffer, Options, Callback); {ok, {http_request, Method, RawPath, Version}, Rest} -> {Method, RawPath, Version, Rest}; {ok, {http_error, _}, _} -> @@ -436,6 +426,21 @@ get_request(Socket, Buffer, Options, {Mod, Args} = Callback) -> exit(normal) end. +recv_request(Socket, Buffer, Options, {Mod, Args} = _Callback) -> + case elli_tcp:recv(Socket, 0, request_timeout(Options)) of + {ok, Data} -> + <>; + {error, timeout} -> + handle_event(Mod, request_timeout, [], Args), + elli_tcp:close(Socket), + exit(normal); + {error, Closed} when Closed =:= closed orelse + Closed =:= enotconn -> + handle_event(Mod, request_closed, [], Args), + elli_tcp:close(Socket), + exit(normal) + end. + -spec get_headers(Socket, V, Buffer, Opts, Callback) -> Headers when Socket :: elli_tcp:socket(), V :: version(), @@ -544,8 +549,7 @@ maybe_send_continue(Socket, Headers) -> % headers contains "Expect:100-continue" case proplists:get_value(<<"Expect">>, Headers, undefined) of <<"100-continue">> -> - Response = [<<"HTTP/1.1 ">>, status(100), <<"\r\n">>, - <<"Content-Length: 0">>, <<"\r\n\r\n">>], + Response = http_response(100), elli_tcp:send(Socket, Response); _Other -> ok @@ -561,19 +565,18 @@ check_max_size(Socket, ContentLength, Buffer, Opts, {Mod, Args}) -> do_check_max_size(Socket, ContentLength, Buffer, MaxSize, {Mod, Args}) when ContentLength > MaxSize -> handle_event(Mod, bad_request, [{body_size, ContentLength}], Args), - do_check_max_size_2x(Socket, ContentLength, Buffer, MaxSize), + do_check_max_size_x2(Socket, ContentLength, Buffer, MaxSize), elli_tcp:close(Socket), exit(normal); do_check_max_size(_, _, _, _, _) -> ok. -do_check_max_size_2x(Socket, ContentLength, Buffer, MaxSize) +do_check_max_size_x2(Socket, ContentLength, Buffer, MaxSize) when ContentLength < MaxSize * 2 -> OnSocket = ContentLength - size(Buffer), elli_tcp:recv(Socket, OnSocket, 60000), - Response = [<<"HTTP/1.1 ">>, status(413), <<"\r\n">>, - <<"Content-Length: 0">>, <<"\r\n\r\n">>], + Response = http_response(413), elli_tcp:send(Socket, Response); -do_check_max_size_2x(_, _, _, _) -> ok. +do_check_max_size_x2(_, _, _, _) -> ok. -spec mk_req(Method, PathTuple, Headers, Body, V, Socket, Callback) -> Req when Method :: elli:http_method(), @@ -603,9 +606,20 @@ mk_req(Method, RawPath, Headers, Body, V, Socket, {Mod, Args} = Callback) -> %% HEADERS %% +http_response(Code) -> + http_response(Code, <<>>). + +http_response(Code, Body) -> + http_response(Code, [{<<"Content-Length">>, size(Body)}], Body). + +http_response(Code, Headers, <<>>) -> + [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>, + encode_headers(Headers), <<"\r\n">>]; +http_response(Code, Headers, Body) -> + [http_response(Code, Headers, <<>>), Body]. + assemble_response_headers(Code, Headers) -> - ResponseHeaders = [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>, - encode_headers(Headers), <<"\r\n">>], + ResponseHeaders = http_response(Code, Headers, <<>>), s(resp_headers, iolist_size(ResponseHeaders)), ResponseHeaders. @@ -849,6 +863,6 @@ get_body_test() -> Buffer = binary:copy(<<".">>, 42), Opts = [], Callback = {no, op}, - ?assertEqual({Buffer, <<>>}, + ?assertMatch({Buffer, <<>>}, get_body(Socket, Headers, Buffer, Opts, Callback)). -endif. diff --git a/src/elli_test.erl b/src/elli_test.erl index 7b7298a..8ef375a 100644 --- a/src/elli_test.erl +++ b/src/elli_test.erl @@ -12,7 +12,7 @@ -export([call/5]). --spec call(Method, Path, Headers, Body, Opts) -> elli:req() when +-spec call(Method, Path, Headers, Body, Opts) -> elli_handler:result() when Method :: elli:http_method(), Path :: binary(), Headers :: elli:headers(), @@ -30,13 +30,13 @@ call(Method, Path, Headers, Body, Opts) -> -include_lib("eunit/include/eunit.hrl"). hello_world_test() -> - ?assertEqual({ok, [], <<"Hello World!">>}, + ?assertMatch({ok, [], <<"Hello World!">>}, elli_test:call('GET', <<"/hello/world/">>, [], <<>>, ?EXAMPLE_CONF)), - ?assertEqual({ok, [], <<"Hello Test1">>}, + ?assertMatch({ok, [], <<"Hello Test1">>}, elli_test:call('GET', <<"/hello/?name=Test1">>, [], <<>>, ?EXAMPLE_CONF)), - ?assertEqual({ok, + ?assertMatch({ok, [{<<"Content-type">>, <<"application/json; charset=ISO-8859-1">>}], <<"{\"name\" : \"Test2\"}">>}, diff --git a/src/elli_util.erl b/src/elli_util.erl index e5391c1..60ff3e2 100644 --- a/src/elli_util.erl +++ b/src/elli_util.erl @@ -54,12 +54,16 @@ encode_range_bytes({Offset, Length}) -> encode_range_bytes(invalid_range) -> <<"*">>. --spec file_size(Filename::file:name()) -> - non_neg_integer() | {error, Reason} - when Reason :: badarg | file:posix(). +-spec file_size(Filename) -> Size | {error, Reason} when + Filename :: file:name_all(), + Size :: non_neg_integer(), + Reason :: file:posix() | badarg | invalid_file. %% @doc: Get the size in bytes of the file. file_size(Filename) -> case file:read_file_info(Filename) of - {ok, #file_info{size = Size}} -> Size; - {error, Reason} -> {error, Reason} + {ok, #file_info{type = regular, access = Perm, size = Size}} + when Perm =:= read orelse Perm =:= read_write -> + Size; + {error, Reason} -> {error, Reason}; + _ -> {error, invalid_file} end. diff --git a/test/elli_metrics_middleware.erl b/test/elli_metrics_middleware.erl index dd124d9..3861c68 100644 --- a/test/elli_metrics_middleware.erl +++ b/test/elli_metrics_middleware.erl @@ -1,5 +1,5 @@ -module(elli_metrics_middleware). --export([handle/2, handle_event/3]). +-export([init/2, preprocess/2, handle/2, postprocess/3, handle_event/3]). -behaviour(elli_handler). @@ -7,9 +7,18 @@ %% ELLI %% +init(_Req, _Args) -> + ignore. + +preprocess(Req, _Args) -> + Req. + handle(_Req, _Args) -> ignore. +postprocess(_Req, Res, _Args) -> + Res. + %% %% ELLI EVENT CALLBACKS diff --git a/test/elli_ssl_tests.erl b/test/elli_ssl_tests.erl index b6fde74..bd002c3 100644 --- a/test/elli_ssl_tests.erl +++ b/test/elli_ssl_tests.erl @@ -6,7 +6,8 @@ elli_ssl_test_() -> {setup, fun setup/0, fun teardown/1, [ - ?_test(hello_world()) + ?_test(hello_world()), + ?_test(chunked()) ]}. %%% Tests @@ -15,6 +16,19 @@ hello_world() -> {ok, Response} = httpc:request("https://localhost:3443/hello/world"), ?assertMatch(200, status(Response)). +chunked() -> + Expected = "chunk10chunk9chunk8chunk7chunk6chunk5chunk4chunk3chunk2chunk1", + + {ok, Response} = httpc:request("https://localhost:3443/chunked"), + + ?assertMatch(200, status(Response)), + ?assertEqual([{"connection", "Keep-Alive"}, + %% httpc adds a content-length, even though elli + %% does not send any for chunked transfers + {"content-length", integer_to_list(length(Expected))}, + {"content-type", "text/event-stream"}], headers(Response)), + ?assertMatch(Expected, body(Response)). + %%% Internal helpers setup() -> diff --git a/test/elli_tests.erl b/test/elli_tests.erl index b2c61af..7259ae0 100644 --- a/test/elli_tests.erl +++ b/test/elli_tests.erl @@ -1,13 +1,14 @@ -module(elli_tests). -include_lib("eunit/include/eunit.hrl"). -include("elli.hrl"). +-include("elli_test.hrl"). -define(I2B(I), list_to_binary(integer_to_list(I))). -define(I2L(I), integer_to_list(I)). -define(README, "README.md"). -define(VTB(T1, T2, LB, UB), - time_diff_to_micro_seconds(T1, T2) > LB andalso - time_diff_to_micro_seconds(T1, T2) < UB). + time_diff_to_micro_seconds(T1, T2) >= LB andalso + time_diff_to_micro_seconds(T1, T2) =< UB). time_diff_to_micro_seconds(T1, T2) -> erlang:convert_time_unit( @@ -22,11 +23,15 @@ elli_test_() -> [{foreach, fun init_stats/0, fun clear_stats/1, [?_test(hello_world()), + ?_test(keep_alive_timings()), ?_test(not_found()), ?_test(crash()), ?_test(invalid_return()), ?_test(no_compress()), + ?_test(gzip()), + ?_test(deflate()), ?_test(exception_flow()), + ?_test(hello_iolist()), ?_test(accept_content_type()), ?_test(user_connection()), ?_test(get_args()), @@ -34,12 +39,15 @@ elli_test_() -> ?_test(decoded_get_args_list()), ?_test(post_args()), ?_test(shorthand()), + ?_test(ip()), + ?_test(found()), ?_test(too_many_headers()), ?_test(too_big_body()), ?_test(way_too_big_body()), ?_test(bad_request_line()), ?_test(content_length()), ?_test(user_content_length()), + ?_test(headers()), ?_test(chunked()), ?_test(sendfile()), ?_test(send_no_file()), @@ -65,11 +73,13 @@ setup() -> application:start(crypto), application:start(public_key), application:start(ssl), + hackney:start(), inets:start(), Config = [ {mods, [ {elli_metrics_middleware, []}, + {elli_middleware_compress, []}, {elli_example_callback, []} ]} ], @@ -87,6 +97,43 @@ init_stats() -> clear_stats(_) -> ets:delete(elli_stat_table). + + +accessors_test_() -> + RawPath = <<"/foo/bar">>, + Headers = [{<<"Content-Type">>, <<"application/x-www-form-urlencoded">>}], + Method = 'POST', + Body = <<"name=knut%3D">>, + Name = <<"knut=">>, + Req1 = #req{raw_path = RawPath, + headers = Headers, + method = Method, + body = Body}, + Args = [{<<"name">>, Name}], + Req2 = #req{headers = Headers, args = Args, body = <<>>}, + + [ + %% POST /foo/bar + ?_assertMatch(RawPath, elli_request:raw_path(Req1)), + ?_assertMatch(Headers, elli_request:headers(Req1)), + ?_assertMatch(Method, elli_request:method(Req1)), + ?_assertMatch(Body, elli_request:body(Req1)), + ?_assertMatch(Args, elli_request:post_args_decoded(Req1)), + ?_assertMatch(undefined, elli_request:post_arg(<<"foo">>, Req1)), + ?_assertMatch(undefined, elli_request:post_arg_decoded(<<"foo">>, Req1)), + ?_assertMatch(Name, elli_request:post_arg_decoded(<<"name">>, Req1)), + %% GET /foo/bar + ?_assertMatch(Headers, elli_request:headers(Req2)), + + ?_assertMatch(Args, elli_request:get_args(Req2)), + ?_assertMatch(undefined, elli_request:get_arg_decoded(<<"foo">>, Req2)), + ?_assertMatch(Name, elli_request:get_arg_decoded(<<"name">>, Req2)), + ?_assertMatch([], elli_request:post_args(Req2)), + + ?_assertMatch({error, not_supported}, elli_request:chunk_ref(#req{})) + ]. + + %%% Integration tests %%% Use inets httpc to actually call Elli over the network. @@ -122,6 +169,65 @@ hello_world() -> ?assertMatch(true, ?VTB(send_start, send_end, 1, 200)). + +keep_alive_timings() -> + + Transport = hackney_tcp, + Host = <<"localhost">>, + Port = 3001, + Options = [], + {ok, ConnRef} = hackney:connect(Transport, Host, Port, Options), + + ReqBody = <<>>, + ReqHeaders = [], + ReqPath = <<"/hello/world">>, + ReqMethod = get, + Req = {ReqMethod, ReqPath, ReqHeaders, ReqBody}, + + {ok, Status, Headers, HCRef} = hackney:send_request(ConnRef, Req), + keep_alive_timings(Status, Headers, HCRef), + + %% pause between keep-alive requests, + %% request_start is a timestamp of + %% the first bytes of the second request + timer:sleep(1000), + + {ok, Status, Headers, HCRef} = hackney:send_request(ConnRef, Req), + keep_alive_timings(Status, Headers, HCRef), + + hackney:close(ConnRef). + +keep_alive_timings(Status, Headers, HCRef) -> + ?assertMatch(200, Status), + ?assertMatch([{<<"Connection">>,<<"Keep-Alive">>}, + {<<"Content-Length">>,<<"12">>}], Headers), + ?assertMatch({ok, <<"Hello World!">>}, hackney:body(HCRef)), + %% sizes + ?assertMatch(63, get_size_value(resp_headers)), + ?assertMatch(12, get_size_value(resp_body)), + %% timings + ?assertNotMatch(undefined, get_timing_value(request_start)), + ?assertNotMatch(undefined, get_timing_value(headers_start)), + ?assertNotMatch(undefined, get_timing_value(headers_end)), + ?assertNotMatch(undefined, get_timing_value(body_start)), + ?assertNotMatch(undefined, get_timing_value(body_end)), + ?assertNotMatch(undefined, get_timing_value(user_start)), + ?assertNotMatch(undefined, get_timing_value(user_end)), + ?assertNotMatch(undefined, get_timing_value(send_start)), + ?assertNotMatch(undefined, get_timing_value(send_end)), + ?assertNotMatch(undefined, get_timing_value(request_end)), + %% check timings + ?assertMatch(true, + ?VTB(request_start, request_end, 1000000, 1200000)), + ?assertMatch(true, + ?VTB(headers_start, headers_end, 1, 100)), + ?assertMatch(true, + ?VTB(body_start, body_end, 1, 100)), + ?assertMatch(true, + ?VTB(user_start, user_end, 1000000, 1200000)), + ?assertMatch(true, + ?VTB(send_start, send_end, 1, 200)). + not_found() -> {ok, Response} = httpc:request("http://localhost:3001/foobarbaz"), ?assertMatch(404, status(Response)), @@ -145,15 +251,31 @@ invalid_return() -> ?assertMatch("Internal server error", body(Response)). no_compress() -> - {ok, Response} = httpc:request(get, {"http://localhost:3001/compressed", - [{"Accept-Encoding", "gzip"}]}, - [], []), + {ok, Response} = httpc:request("http://localhost:3001/compressed"), ?assertMatch(200, status(Response)), ?assertMatch([{"connection", "Keep-Alive"}, {"content-length", "1032"}], headers(Response)), ?assertEqual(binary:copy(<<"Hello World!">>, 86), list_to_binary(body(Response))). +compress(Encoding, Length) -> + {ok, Response} = httpc:request(get, {"http://localhost:3001/compressed", + [{"Accept-Encoding", Encoding}]}, + [], []), + ?assertMatch(200, status(Response)), + ?assertMatch([{"connection", "Keep-Alive"}, + {"content-encoding", Encoding}, + {"content-length", Length}], headers(Response)), + ?assertEqual(binary:copy(<<"Hello World!">>, 86), + uncompress(Encoding, body(Response))). + +uncompress("gzip", Data) -> zlib:gunzip(Data); +uncompress("deflate", Data) -> zlib:uncompress(Data). + +gzip() -> compress("gzip", "41"). + +deflate() -> compress("deflate", "29"). + exception_flow() -> {ok, Response} = httpc:request("http://localhost:3001/403"), ?assertMatch(403, status(Response)), @@ -161,6 +283,11 @@ exception_flow() -> {"content-length", "9"}], headers(Response)), ?assertMatch("Forbidden", body(Response)). +hello_iolist() -> + Url = "http://localhost:3001/hello/iolist?name=knut", + {ok, Response} = httpc:request(Url), + ?assertMatch("Hello knut", body(Response)). + accept_content_type() -> {ok, Json} = httpc:request(get, {"http://localhost:3001/type?name=knut", [{"Accept", "application/json"}]}, [], []), @@ -210,6 +337,20 @@ shorthand() -> {"content-length", "5"}], headers(Response)), ?assertMatch("hello", body(Response)). +ip() -> + {ok, Response} = httpc:request("http://localhost:3001/ip"), + ?assertMatch(200, status(Response)), + ?assertMatch("127.0.0.1", body(Response)). + +found() -> + {ok, Response} = httpc:request(get, {"http://localhost:3001/302", []}, + [{autoredirect, false}], []), + ?assertMatch(302, status(Response)), + ?assertMatch([{"connection","Keep-Alive"}, + {"content-length","0"}, + {"location", "/hello/world"}], headers(Response)), + ?assertMatch("", body(Response)). + too_many_headers() -> Headers = lists:duplicate(100, {"X-Foo", "Bar"}), {ok, Response} = httpc:request(get, {"http://localhost:3001/foo", Headers}, @@ -262,6 +403,12 @@ user_content_length() -> "foobar">>}, gen_tcp:recv(Client, 0)). +headers() -> + {ok, Response} = httpc:request("http://localhost:3001/headers.html"), + Headers = headers(Response), + + ?assert(proplists:is_defined("x-custom", Headers)), + ?assertMatch("foobar", proplists:get_value("x-custom", Headers)). chunked() -> Expected = "chunk10chunk9chunk8chunk7chunk6chunk5chunk4chunk3chunk2chunk1", @@ -579,6 +726,10 @@ normalize_range_test_() -> ?_assertMatch(invalid_range, elli_util:normalize_range(Invalid6, Size))]. +encode_range_test() -> + Expected = [<<"bytes ">>,<<"*">>,<<"/">>,"42"], + ?assertMatch(Expected, elli_util:encode_range(invalid_range, 42)). + register_test() -> ?assertMatch(undefined, whereis(elli)), Config = [ @@ -598,14 +749,3 @@ invalid_callback_test() -> E -> ?assertMatch(invalid_callback, E) end. - - -%%% Helpers - -status({{_, Status, _}, _, _}) -> - Status. -body({_, _, Body}) -> - Body. - -headers({_, Headers, _}) -> - lists:sort(Headers).
adder
elli
elli_example_callback
elli_example_callback_handover