Packages

KATT (Klarna API Testing Tool) is an HTTP-based API testing tool for Erlang.

Current section

Files

Jump to
katt src katt_callbacks.erl
Raw

src/katt_callbacks.erl

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Copyright 2012- Klarna AB
%%% Copyright 2014- AUTHORS
%%%
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%% http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.
%%%
%%% @copyright 2012- Klarna AB, AUTHORS
%%%
%%% @doc Built-in default callback functions.
%%% @end
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%_* Module declaration =======================================================
-module(katt_callbacks).
%%%_* Exports ==================================================================
%% API
-export([ ext/1
, recall/4
, parse/4
, request/3
, validate/4
, validate_type/6
, progress/2
, text_diff/2
, transform/4
]).
%%%_* Includes =================================================================
-include("katt.hrl").
%%%_* API ======================================================================
%% @doc Get a list of available extensions
%% @end
-spec ext(any()) -> list().
-ifdef(BARE_MODE).
ext(_) -> [].
-else.
ext(recall_body) ->
[ fun katt_callbacks_json:recall_body/4
, fun katt_callbacks_wfu:recall_body/4
];
ext(parse) ->
[ fun katt_callbacks_json:parse/5
, fun katt_callbacks_wfu:parse/5
];
ext(validate_body) ->
[ fun katt_callbacks_json:validate_body/4
, fun katt_callbacks_wfu:validate_body/4
];
ext(validate_type) ->
[ fun katt_callbacks_json:validate_type/7
, fun katt_callbacks_wfu:validate_type/7
].
-endif.
%% @doc Recall all params inside url/status/headers/body/text content.
%% @end
-spec recall( recall_scope()
, any()
, params()
, callbacks()
) -> any().
recall(_Scope, null, _Params, _Callbacks) ->
null;
recall(_Scope, Input, [], _Callbacks) ->
Input;
recall(text, Bin, [{_K, [H|_]} | Next], Callbacks) when is_tuple(H) ->
recall(text, Bin, Next, Callbacks);
recall(text, Bin0, [{K0, V} | Next], Callbacks) ->
K = ?RECALL_BEGIN_TAG ++ K0 ++ ?RECALL_END_TAG,
REK = katt_util:escape_regex(K),
REV = katt_util:escape_regex(V),
Bin = re:replace( Bin0
, REK
, REV
, [{return, binary}, global]),
recall(text, Bin, Next, Callbacks);
recall(url, Bin, Params, Callbacks) ->
recall(text, Bin, Params, Callbacks);
recall(headers, Hdrs0, Params, Callbacks) ->
[ { K
, katt_util:from_utf8(
recall(text, katt_util:to_utf8(V), Params, Callbacks)
)
}
|| {K, V} <- Hdrs0
];
recall(body, [Hdrs, Bin], Params, Callbacks) ->
ExtFun = proplists:get_value(ext, Callbacks),
Ext = ExtFun(recall_body),
MatchingExt = lists:dropwhile( fun(Fun) ->
not Fun( _JustCheck = true
, [Hdrs, Bin]
, Params
, Callbacks
)
end
, Ext
),
case MatchingExt of
[] ->
[ Hdrs
, recall(text, Bin, Params, Callbacks)
];
[Fun|_] ->
[ Hdrs
, Fun( _JustCheck = false
, [Hdrs, Bin]
, Params
, Callbacks
)
]
end.
%% @doc Parse the body of e.g. an HTTP response.
%% @end
-spec parse( headers()
, body()
, params()
, callbacks()
) -> any().
parse(_Hdrs, null, _Params, _Callbacks) ->
[];
parse(Hdrs, Body, Params, Callbacks) ->
ExtFun = proplists:get_value(ext, Callbacks),
Ext = ExtFun(parse),
MatchingExt = lists:dropwhile( fun(Fun) ->
not Fun( _JustCheck = true
, Hdrs
, Body
, Params
, Callbacks
)
end
, Ext),
case MatchingExt of
[] ->
Body;
[Fun|_] ->
Fun( _JustCheck = false
, Hdrs
, Body
, Params
, Callbacks
)
end.
%% @doc Make a request, e.g. an HTTP request.
%% @end
-spec request( request()
, params()
, callbacks()
) -> response().
request(R = #katt_request{}, Params, _Callbacks) ->
case http_request(R, Params) of
{ok, {{Code, _}, Hdrs, Body}} ->
#katt_response{ status = Code
, headers = Hdrs
, body = Body
};
Error = {error, _} ->
Error
end.
-dialyzer({no_match, request/3}). % support bare mode for http_request -> katt_util:external_http_request
%% @doc Validate a response.
%% @end
-spec validate( response()
, response()
, params()
, callbacks()
) -> {pass, details()} | {fail, details()}.
validate( Expected = #katt_response{}
, Actual = #katt_response{}
, _Params
, Callbacks) ->
{AddParams0, Failures0} = katt_util:get_params_and_failures(
validate_status(Expected, Actual, Callbacks)),
{AddParams1, Failures1} = katt_util:get_params_and_failures(
validate_headers(Expected, Actual, Callbacks)),
AddParams2 = katt_util:merge_proplists(AddParams0, AddParams1),
{AddParams3, Failures2} = katt_util:get_params_and_failures(
validate_body(Expected, Actual, Callbacks)),
AddParams = katt_util:merge_proplists(AddParams2, AddParams3),
Failures = Failures0 ++ Failures1 ++ Failures2,
case Failures of
[] -> {pass, AddParams};
_ -> {fail, Failures}
end;
validate(Expected, #katt_response{}, _Params, _Callbacks) -> {fail, Expected};
validate(#katt_response{}, Actual, _Params, _Callbacks) -> {fail, Actual}.
%% @doc Notify of scenario progress
%% @end
-spec progress( term()
, any()
) -> ok.
progress(_Step, _Detail) ->
ok.
%% @doc Perform a text diff
%% @end
-spec text_diff( list()
, list()
) -> proplists:proplist().
-ifdef(BARE_MODE).
text_diff(_A, _B) ->
[].
-else.
text_diff(A, B) ->
[{text_diff, tdiff:diff(A, B)}].
-endif.
%% @doc Transform a request or a response
%% @end
-spec transform( list()
, request() | {response(), list()}
, params()
, callbacks()
) -> request() | response().
transform(_Id, #katt_request{} = Req, _Params, _Callbacks) ->
Req;
transform(_Id, {#katt_response{} = Res, _Actual}, _Params, _Callbacks) ->
Res.
%%%_* Internal =================================================================
http_request( #katt_request{ method = Method
, url = Url
, headers = Hdrs0
, body = Body0
} = _R
, Params) ->
Body = case Body0 of
null -> <<>>;
Bin -> Bin
end,
Sleep = case proplists:get_value("x-katt-request-sleep", Hdrs0) of
undefined ->
0;
SleepStr ->
list_to_integer(SleepStr)
end,
Timeout = case proplists:get_value("x-katt-request-timeout", Hdrs0) of
undefined ->
proplists:get_value("request_timeout", Params);
TimeoutStr ->
list_to_integer(TimeoutStr)
end,
Hdrs1 = proplists:delete("x-katt-request-sleep", Hdrs0),
Hdrs = proplists:delete("x-katt-request-timeout", Hdrs1),
Options = proplists:get_value("hackney_options", Params, []),
timer:sleep(Sleep),
katt_util:external_http_request(Url, Method, Hdrs, Body, Timeout, Options).
validate_status( #katt_response{status=E}
, #katt_response{status=A}
, _Callbacks
) ->
katt_util:validate("/status", E, A).
%% Actual headers are allowed to be a superset of expected headers, since
%% we don't want tests full of boilerplate like tests for headers such as
%% Content-Length, Server, Date, etc.
%% The header name (not the value) is compared case-insensitive
validate_headers( #katt_response{headers=E0}
, #katt_response{headers=A0}
, Callbacks
) ->
LowerE = [{katt_util:to_lower(K), V} || {K, V} <- E0],
LowerA = [{katt_util:to_lower(K), V} || {K, V} <- A0],
%% From RFC 2616:
%% Multiple message-header fields with the same field-name MAY be present in a
%% message if and only if the entire field-value for that header field is
%% defined as a comma-separated list [i.e., #(values)]. It MUST be possible to
%% combine the multiple header fields into one "field-name: field-value" pair,
%% without changing the semantics of the message,
%% by appending each subsequent field-value to the first,
%% each separated by a comma. The order in which header fields with the same
%% field-name are received is therefore significant to the interpretation of
%% the combined field value,
%% and thus a proxy MUST NOT change the order of these field values when a
%% message is forwarded.
%%
%% We are liberal here and assume all duplicate headers were defined
%% as a comma-separated list
ConcatenatedE = lists:map( fun(Header) ->
{Header, concatenate_header(Header, LowerE)}
end
, proplists:get_keys(LowerE)
),
ConcatenatedA = lists:map( fun(Header) ->
{Header, concatenate_header(Header, LowerA)}
end
, proplists:get_keys(LowerA)
),
E = {struct, ConcatenatedE},
A = {struct, ConcatenatedA},
katt_util:validate("/headers", E, A, ?MATCH_ANY, Callbacks).
concatenate_header(Header, Headers) ->
Values = proplists:get_all_values(Header, Headers),
string:join(Values, ", ").
%% Bodies are also allowed to be a superset of expected body, if the parseFun
%% returns a structure.
validate_body( #katt_response{parsed_body=E} = ER
, #katt_response{parsed_body=A} = AR
, Callbacks
) ->
ExtFun = proplists:get_value(ext, Callbacks),
Ext = ExtFun(validate_body),
MatchingExt = lists:dropwhile( fun(Fun) ->
not Fun( _JustCheck = true
, ER
, AR
, Callbacks
)
end
, Ext
),
case MatchingExt of
[] ->
katt_util:validate("/body", E, A, ?MATCH_ANY, Callbacks);
[Fun|_] ->
Fun( _JustCheck = false
, ER
, AR
, Callbacks
)
end.
validate_type( Type
, ParentKey
, Expected
, Actual
, ItemsMode
, Callbacks
) ->
ExtFun = proplists:get_value(ext, Callbacks),
Ext = ExtFun(validate_type),
MatchingExt = lists:dropwhile( fun(Fun) ->
not Fun( _JustCheck = true
, Type
, ParentKey
, Expected
, Actual
, ItemsMode
, Callbacks
)
end
, Ext
),
case MatchingExt of
[] ->
fail;
[Fun|_] ->
Fun( _JustCheck = false
, Type
, ParentKey
, Expected
, Actual
, ItemsMode
, Callbacks
)
end.