Packages

Erlang wrapper for Pact. Pact is a contract testing framework for HTTP APIs and non-HTTP asynchronous messaging systems.

Current section

Files

Jump to
pact_erlang src pact_verifier.erl
Raw

src/pact_verifier.erl

-module(pact_verifier).
-include_lib("inets/include/httpd.hrl").
-behaviour(gen_server).
%% Internal web server API
-export([
start/2,
stop/1,
do/1
]).
%% Pact Verifier APIs
-export([
start_verifier/2,
verify/1,
stop_verifier/1
]).
-export([init/1, handle_call/3, terminate/2, handle_cast/2]).
-dialyzer(no_behaviours).
start(Port, Name) ->
{ok, Pid} = inets:start(httpd, [
{bind_address, "127.0.0.1"},
{port, Port},
{server_name, binary_to_list(Name)},
{server_root, "./"},
{document_root, "./"},
{modules, [pact_verifier]}
]),
Info = httpd:info(Pid),
{port, ListenPort} = lists:keyfind(port, 1, Info),
{ok, ListenPort, Pid}.
stop(Pid) ->
inets:stop(httpd, Pid).
do(ModData) ->
case catch process_data(ModData) of
{'EXIT', Reason} ->
io:format("Error: ~p~n", [Reason]),
[{response, {500, "Internal Server Error"}}];
Response ->
Response
end.
process_data(
#mod{request_uri = "/message_pact/verify", method = "POST", entity_body = Body} = ModData
) ->
{ok, StateReq} = thoas:decode(Body),
Description = maps:get(<<"description">>, StateReq, <<"">>),
Port = extract_address_port(ModData#mod.absolute_uri),
Info = httpd:info({127, 0, 0, 1}, Port),
{server_name, ServerName} = lists:keyfind(server_name, 1, Info),
{M, F, Args} = get_mfa_from_description(ServerName, Description),
Message = erlang:apply(M, F, Args),
make_json_response(200, Message).
make_json_response(Code, Body) ->
BodyJson = erlang:binary_to_list(thoas:encode(Body)),
Length = io_lib:format("~w", [io_lib:chars_length(BodyJson)]),
{proceed, [
{response,
{response, [{code, Code}, {content_length, Length}, {content_type, "application/json"}],
BodyJson}}
]}.
%% Web Server End
%% ================================================================================================
%% Gen Server
-type provider() :: binary().
-type provider_opts() :: map().
-type verifier_ref() :: pid().
%% erlfmt-ignore
-record(pact_verifier, {
provider :: provider(),
provider_opts = #{} :: provider_opts(),
provider_port_details :: tuple()
}).
%% @doc Starts pact provider verifier
-spec start_verifier(provider(), provider_opts()) -> gen_server:start_ret().
start_verifier(Provider, ProviderOpts) ->
Protocol = maps:get(protocol, ProviderOpts, <<"http">>),
ProviderPort = maps:get(port, ProviderOpts, undefined),
{Port, HttpPid} =
case Protocol of
<<"http">> ->
Port1 = ProviderPort,
{Port1, undefined};
<<"message">> ->
case ProviderPort of
undefined ->
%% Start a new port for message protocol
{ok, Port1, HttpPid1} = pact_verifier:start(0, Provider),
{Port1, HttpPid1};
Port1 ->
{Port1, undefined}
end
end,
gen_server:start(
{global, {?MODULE, Provider}},
?MODULE,
#pact_verifier{
provider = Provider,
provider_opts = ProviderOpts,
provider_port_details = {Port, HttpPid}
},
[]
).
%% @doc Verifies pacts for the given provider
%% Returns 0 if all pacts are verified successfully
-spec verify(verifier_ref()) -> integer().
verify(VerifierRef) ->
{ProviderOpts, ProviderPortDetails} = gen_server:call(VerifierRef, {get_provider_state_details}),
verify_pacts_internal(VerifierRef, ProviderOpts, ProviderPortDetails).
-spec get_mfa_from_description(string(), binary()) -> tuple().
get_mfa_from_description(Provider, Description) ->
{Map, FallbackProviderMFA} = gen_server:call({global, {?MODULE, list_to_binary(Provider)}}, {
get_message_providers_map
}),
case maps:get(Description, Map, undefined) of
undefined ->
FallbackProviderMFA;
ProviderMFA ->
ProviderMFA
end.
stop_verifier(VerifierRef) ->
ok = gen_server:stop(VerifierRef).
%% message_providers map example
%% Sample description to MFA mapping for pact verifier to know which MFA to test
% #{
% <<"weather api 1">> => {M, F, ArgsList}
% }
%% Gen_server callbacks
init(#pact_verifier{provider = _Provider, provider_opts = _ProviderOpts} = State) ->
{ok, State}.
handle_call({get_provider_state_details}, _From, State) ->
%% Verify file pacts or broker pacts or both here
%% Based on pact_source_opts key in Opts.
ProviderOpts = State#pact_verifier.provider_opts,
ProviderPortDetails = State#pact_verifier.provider_port_details,
{reply, {ProviderOpts, ProviderPortDetails}, State};
handle_call({get_message_providers_map}, _From, State) ->
ProviderOpts = State#pact_verifier.provider_opts,
MessageProvidersMap = maps:get(message_providers, ProviderOpts, #{}),
FallbackProviderFunc = maps:get(fallback_message_provider, ProviderOpts, undefined),
{reply, {MessageProvidersMap, FallbackProviderFunc}, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
verify_pacts_internal(VerifierRef, ProviderOpts, ProviderPortDetails) ->
{Port, HttpPid} = ProviderPortDetails,
#{
name := Name,
version := Version,
host := Host,
branch := Branch,
pact_source_opts := PactSourceOpts
} = ProviderOpts,
Protocol = maps:get(protocol, ProviderOpts, <<"http">>),
BaseUrl = maps:get(base_url, ProviderOpts, <<"/">>),
StateChangeUrl = maps:get(state_change_url, ProviderOpts, <<"">>),
Scheme = maps:get(scheme, ProviderOpts, <<"http">>),
FilePath = maps:get(file_path, PactSourceOpts, undefined),
PactBrokerUrl = maps:get(broker_url, PactSourceOpts, undefined),
%% PublishVerificationResults = 1 means publish verification results
%% Otherwise do not publish
PublishVerificationResults = maps:get(publish_verification_results, ProviderOpts, 1),
OutputFileVerification =
case FilePath of
undefined ->
0;
_ ->
pactffi_nif:verify_file_pacts(
Name,
Scheme,
Host,
Port,
BaseUrl,
Version,
Branch,
FilePath,
Protocol,
StateChangeUrl,
PublishVerificationResults
)
end,
OutputBrokerVerification =
case PactBrokerUrl of
undefined ->
0;
_ ->
#{
broker_username := BrokerUser,
broker_password := BrokerPassword,
enable_pending := EnablePending,
consumer_version_selectors := ConsumerVersionSelectors
} = PactSourceOpts,
pactffi_nif:verify_broker_pacts(
Name,
Scheme,
Host,
Port,
BaseUrl,
Version,
Branch,
PactBrokerUrl,
BrokerUser,
BrokerPassword,
EnablePending,
ConsumerVersionSelectors,
Protocol,
StateChangeUrl,
PublishVerificationResults
)
end,
case Protocol of
<<"message">> ->
case HttpPid of
undefined ->
%% If HttpPid is undefined, it means we are not using HTTP protocol
%% and we should not stop the HTTP server.
ok;
_ ->
%% Stop the HTTP server if it was started for message protocol
pact_verifier:stop(HttpPid)
end,
stop_verifier(VerifierRef);
_ ->
stop_verifier(VerifierRef)
end,
combine_return_codes(OutputFileVerification, OutputBrokerVerification).
combine_return_codes(0, 0) -> 0;
combine_return_codes(Code1, _) when Code1 =/= 0 -> Code1;
combine_return_codes(_, Code2) -> Code2.
extract_address_port(Url) ->
% Define a regex pattern to match the address and port
Pattern = ":(\\d+)/",
% Use re module to match the pattern against the URL
case re:run(Url, Pattern, [{capture, all_but_first, list}]) of
{match, [PortStr]} ->
% Convert the port string to an integer
Port = list_to_integer(PortStr),
Port
end.