%%%============================================================================= %%% @copyright (C) 1999-2013, Erlang Solutions Ltd %%% @author Diana Corbacho %%% @doc %%% @end %%%============================================================================= -module(fusco_protocol_eqc). -copyright("2013, Erlang Solutions Ltd."). -include_lib("eqc/include/eqc.hrl"). -include("fusco.hrl"). -export([prop_http_response_close_connection/0, prop_http_response_keep_alive/0, prop_chunked_http_response_keep_alive/0]). %%============================================================================== %% Quickcheck generators %%============================================================================== valid_http_message() -> ?LET({StatusLine, Headers, Cookies}, {http_eqc_gen:status_line(), http_eqc_gen:headers(), list(http_eqc_gen:set_cookie())}, ?LET(Body, http_eqc_encoding:body(StatusLine), {StatusLine, http_eqc_encoding:add_content_length(Headers, Body), Cookies, Body})). valid_http_chunked_message() -> ?LET({StatusLine, Headers, Cookies}, {http_eqc_gen:status_line(), http_eqc_gen:headers(), list(http_eqc_gen:set_cookie())}, ?LET(Body, http_eqc_gen:chunked_body(), {StatusLine, http_eqc_encoding:add_transfer_encoding( Headers, <<"chunked">>), Cookies, Body})). %%============================================================================== %% Quickcheck properties %%============================================================================== prop_http_response_close_connection() -> %% Connection is closed just after send the response prop_http_response(close). prop_http_response_keep_alive() -> %% Connection stays open after send the response prop_http_response(keepalive). prop_chunked_http_response_keep_alive() -> %% Connection stays open after send the response prop_chunked_http_response(keepalive). prop_http_response(ConnectionState) -> eqc:numtests( 500, ?FORALL(ValidMessage, valid_http_message(), decode_valid_message(ConnectionState, ValidMessage))). decode_valid_message(ConnectionState, {StatusLine, Headers, Cookies, Body}) -> Msg = http_eqc_encoding:build_valid_response(StatusLine, Headers, Cookies, Body), L = {_, _, Socket} = test_utils:start_listener({fragmented, Msg}, ConnectionState), test_utils:send_message(Socket), Recv = fusco_protocol:recv(Socket, false), test_utils:stop_listener(L), Expected = expected_output(StatusLine, Headers, Cookies, Body, Msg), Cleared = clear_record(clear_connection(Recv)), ?WHENFAIL(io:format("Message:~n=======~n~s~n=======~nResponse:" " ~p~nCleared: ~p~nExpected: ~p~n", [binary:list_to_bin(Msg), Recv, Cleared, Expected]), case Cleared of Expected -> true; _ -> false end). prop_chunked_http_response(ConnectionState) -> eqc:numtests( 500, ?FORALL(ValidMessage, valid_http_chunked_message(), decode_valid_message(ConnectionState, ValidMessage))). %%============================================================================== %% Internal functions %%============================================================================== expected_output({HttpVersion, StatusCode, Reason}, Headers, Cookies, Body, Msg) -> Version = http_version(HttpVersion), OCookies = [{Name, list_to_binary(http_eqc_encoding:build_cookie(Cookie))} || {Name, Cookie} <- Cookies], LowerHeaders = lists:reverse(headers_to_lower(Headers ++ OCookies)), CookiesRec = output_cookies(Cookies), #response{version = Version, status_code = StatusCode, reason = Reason, cookies = CookiesRec, headers = LowerHeaders, connection = to_lower(proplists:get_value(<<"connection">>, LowerHeaders)), body = expected_body(Body), content_length = content_length(Body), transfer_encoding = to_lower(proplists:get_value(<<"transfer-encoding">>, LowerHeaders)), size = byte_size(list_to_binary(Msg))}. expected_body(Body) when is_binary(Body) -> Body; expected_body(List) -> list_to_binary([Bin || {_, Bin} <- List]). content_length(Body) when is_binary(Body) -> byte_size(Body); content_length(_) -> 0. output_cookies(Cookies) -> output_cookies(Cookies, []). output_cookies([{_SetCookie, {{K, V}, Avs}} | Rest], Acc) -> MaxAge = output_max_age(proplists:get_value(<<"Max-Age">>, Avs)), Path = proplists:get_value(<<"Path">>, Avs), PathTokens = case Path of Bin when is_binary(Bin) -> binary:split(Bin, <<"/">>, [global]); undefined -> undefined end, Expires = http_eqc_encoding:expires_datetime(proplists:get_value(<<"Expires">>, Avs)), Cookie = #fusco_cookie{name = K, value = V, max_age = MaxAge, path = Path, path_tokens = PathTokens, expires = Expires, domain = proplists:get_value(<<"Domain">>, Avs)}, output_cookies(Rest, [Cookie | Acc]); output_cookies([], Acc) -> Acc. output_max_age(undefined) -> undefined; output_max_age(Age) -> list_to_integer(binary_to_list(Age)) * 1000000. clear_record(Response) when is_record(Response, response) -> Response#response{socket = undefined, ssl = undefined, in_timestamp = undefined}; clear_record(Error) -> Error. clear_connection(Response) when is_record(Response, response) -> Response#response{connection = to_lower(Response#response.connection)}; clear_connection(Error) -> Error. http_version(<<"HTTP/1.1">>) -> {1, 1}; http_version(<<"HTTP/1.0">>) -> {1, 0}. headers_to_lower(Headers) -> [begin He = to_lower(H), case He of LH when LH == <<"connection">>; LH == <<"transfer-encoding">> -> {He, to_lower(V)}; _ -> {He, V} end end || {H, V} <- Headers]. to_lower(undefined) -> undefined; to_lower(Bin) -> list_to_binary(string:to_lower(binary_to_list(Bin))).