mirror of
https://github.com/processone/ejabberd.git
synced 2024-12-26 17:38:45 +01:00
a3a33bd5fc
We need to be able to run only a few test groups, even if we do not have all database backends installed and configured locally. ejabberd test suite configures a specific host per backend. I changed ejabberd to allow ignoring some hosts from config file on start, by providing the exact list of hosts we want to start. This is done by setting an ejabberd app Erlang environment variable 'hosts' and passing the list of hosts we want to actually define. When doing so, the backend specific hosts defined in ejabberd test configuration file are simply ignored. As a result, we do not try to connect to unavailable backends. I linked that part to CT run test by defining the hosts list based on environment variable CT_BACKENDS. This variable is expected to be a comma separated list of available backends. When Erlang Common Tests are run with that environment variable set, only the host matching the name of the backend will be set, plus the default "localhost", common to many tests. This can be combined with rebar ct groups list. Example commands to run tests: CT_BACKENDS=riak,mnesia rebar ct suites=ejabberd CT_BACKENDS=mnesia rebar ct suites=ejabberd groups=mnesia
468 lines
16 KiB
Erlang
468 lines
16 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% @author Evgeniy Khramtsov <>
|
|
%%% @copyright (C) 2013-2016, Evgeniy Khramtsov
|
|
%%% @doc
|
|
%%%
|
|
%%% @end
|
|
%%% Created : 27 Jun 2013 by Evgeniy Khramtsov <>
|
|
%%%-------------------------------------------------------------------
|
|
-module(suite).
|
|
|
|
%% API
|
|
-compile(export_all).
|
|
|
|
-include("suite.hrl").
|
|
|
|
%%%===================================================================
|
|
%%% API
|
|
%%%===================================================================
|
|
init_config(Config) ->
|
|
DataDir = proplists:get_value(data_dir, Config),
|
|
PrivDir = proplists:get_value(priv_dir, Config),
|
|
[_, _|Tail] = lists:reverse(filename:split(DataDir)),
|
|
BaseDir = filename:join(lists:reverse(Tail)),
|
|
ConfigPathTpl = filename:join([DataDir, "ejabberd.yml"]),
|
|
LogPath = filename:join([PrivDir, "ejabberd.log"]),
|
|
SASLPath = filename:join([PrivDir, "sasl.log"]),
|
|
MnesiaDir = filename:join([PrivDir, "mnesia"]),
|
|
CertFile = filename:join([DataDir, "cert.pem"]),
|
|
{ok, CWD} = file:get_cwd(),
|
|
{ok, _} = file:copy(CertFile, filename:join([CWD, "cert.pem"])),
|
|
{ok, CfgContentTpl} = file:read_file(ConfigPathTpl),
|
|
CfgContent = process_config_tpl(CfgContentTpl, [
|
|
{c2s_port, 5222},
|
|
{s2s_port, 5269},
|
|
{web_port, 5280},
|
|
{mysql_server, <<"localhost">>},
|
|
{mysql_port, 3306},
|
|
{mysql_db, <<"ejabberd_test">>},
|
|
{mysql_user, <<"ejabberd_test">>},
|
|
{mysql_pass, <<"ejabberd_test">>},
|
|
{pgsql_server, <<"localhost">>},
|
|
{pgsql_port, 5432},
|
|
{pgsql_db, <<"ejabberd_test">>},
|
|
{pgsql_user, <<"ejabberd_test">>},
|
|
{pgsql_pass, <<"ejabberd_test">>}
|
|
]),
|
|
ConfigPath = filename:join([CWD, "ejabberd.yml"]),
|
|
ok = file:write_file(ConfigPath, CfgContent),
|
|
ok = application:load(sasl),
|
|
ok = application:load(mnesia),
|
|
ok = application:load(ejabberd),
|
|
application:set_env(ejabberd, config, ConfigPath),
|
|
application:set_env(ejabberd, log_path, LogPath),
|
|
application:set_env(sasl, sasl_error_logger, {file, SASLPath}),
|
|
application:set_env(mnesia, dir, MnesiaDir),
|
|
[{server_port, ct:get_config(c2s_port, 5222)},
|
|
{server_host, "localhost"},
|
|
{server, ?COMMON_VHOST},
|
|
{user, <<"test_single">>},
|
|
{master_nick, <<"master_nick">>},
|
|
{slave_nick, <<"slave_nick">>},
|
|
{room_subject, <<"hello, world!">>},
|
|
{certfile, CertFile},
|
|
{base_dir, BaseDir},
|
|
{resource, <<"resource">>},
|
|
{master_resource, <<"master_resource">>},
|
|
{slave_resource, <<"slave_resource">>},
|
|
{password, <<"password">>},
|
|
{backends, get_config_backends()}
|
|
|Config].
|
|
|
|
%% Read environment variable CT_DB=riak,mysql to limit the backends to test.
|
|
%% You can thus limit the backend you want to test with:
|
|
%% CT_BACKENDS=riak,mysql rebar ct suites=ejabberd
|
|
get_config_backends() ->
|
|
case os:getenv("CT_BACKENDS") of
|
|
false -> all;
|
|
String ->
|
|
Backends0 = string:tokens(String, ","),
|
|
lists:map(fun(Backend) -> string:strip(Backend, both, $ ) end, Backends0)
|
|
end.
|
|
|
|
process_config_tpl(Content, []) ->
|
|
Content;
|
|
process_config_tpl(Content, [{Name, DefaultValue} | Rest]) ->
|
|
Val = case ct:get_config(Name, DefaultValue) of
|
|
V1 when is_integer(V1) ->
|
|
integer_to_binary(V1);
|
|
V2 when is_atom(V2) ->
|
|
atom_to_binary(V2, latin1);
|
|
V3 ->
|
|
V3
|
|
end,
|
|
NewContent = binary:replace(Content, <<"@@",(atom_to_binary(Name, latin1))/binary, "@@">>, Val),
|
|
process_config_tpl(NewContent, Rest).
|
|
|
|
|
|
connect(Config) ->
|
|
{ok, Sock} = ejabberd_socket:connect(
|
|
?config(server_host, Config),
|
|
?config(server_port, Config),
|
|
[binary, {packet, 0}, {active, false}]),
|
|
init_stream(set_opt(socket, Sock, Config)).
|
|
|
|
init_stream(Config) ->
|
|
ok = send_text(Config, io_lib:format(?STREAM_HEADER,
|
|
[?config(server, Config)])),
|
|
{xmlstreamstart, <<"stream:stream">>, Attrs} = recv(),
|
|
<<"jabber:client">> = fxml:get_attr_s(<<"xmlns">>, Attrs),
|
|
<<"1.0">> = fxml:get_attr_s(<<"version">>, Attrs),
|
|
#stream_features{sub_els = Fs} = recv(),
|
|
Mechs = lists:flatmap(
|
|
fun(#sasl_mechanisms{list = Ms}) ->
|
|
Ms;
|
|
(_) ->
|
|
[]
|
|
end, Fs),
|
|
lists:foldl(
|
|
fun(#feature_register{}, Acc) ->
|
|
set_opt(register, true, Acc);
|
|
(#starttls{}, Acc) ->
|
|
set_opt(starttls, true, Acc);
|
|
(#compression{methods = Ms}, Acc) ->
|
|
set_opt(compression, Ms, Acc);
|
|
(_, Acc) ->
|
|
Acc
|
|
end, set_opt(mechs, Mechs, Config), Fs).
|
|
|
|
disconnect(Config) ->
|
|
Socket = ?config(socket, Config),
|
|
ok = ejabberd_socket:send(Socket, ?STREAM_TRAILER),
|
|
{xmlstreamend, <<"stream:stream">>} = recv(),
|
|
ejabberd_socket:close(Socket),
|
|
Config.
|
|
|
|
close_socket(Config) ->
|
|
Socket = ?config(socket, Config),
|
|
ejabberd_socket:close(Socket),
|
|
Config.
|
|
|
|
starttls(Config) ->
|
|
send(Config, #starttls{}),
|
|
#starttls_proceed{} = recv(),
|
|
TLSSocket = ejabberd_socket:starttls(
|
|
?config(socket, Config),
|
|
[{certfile, ?config(certfile, Config)},
|
|
connect]),
|
|
init_stream(set_opt(socket, TLSSocket, Config)).
|
|
|
|
zlib(Config) ->
|
|
send(Config, #compress{methods = [<<"zlib">>]}),
|
|
#compressed{} = recv(),
|
|
ZlibSocket = ejabberd_socket:compress(?config(socket, Config)),
|
|
init_stream(set_opt(socket, ZlibSocket, Config)).
|
|
|
|
auth(Config) ->
|
|
Mechs = ?config(mechs, Config),
|
|
HaveMD5 = lists:member(<<"DIGEST-MD5">>, Mechs),
|
|
HavePLAIN = lists:member(<<"PLAIN">>, Mechs),
|
|
if HavePLAIN ->
|
|
auth_SASL(<<"PLAIN">>, Config);
|
|
HaveMD5 ->
|
|
auth_SASL(<<"DIGEST-MD5">>, Config);
|
|
true ->
|
|
ct:fail(no_sasl_mechanisms_available)
|
|
end.
|
|
|
|
bind(Config) ->
|
|
#iq{type = result, sub_els = [#bind{}]} =
|
|
send_recv(
|
|
Config,
|
|
#iq{type = set,
|
|
sub_els = [#bind{resource = ?config(resource, Config)}]}),
|
|
Config.
|
|
|
|
open_session(Config) ->
|
|
#iq{type = result, sub_els = []} =
|
|
send_recv(Config, #iq{type = set, sub_els = [#session{}]}),
|
|
Config.
|
|
|
|
auth_SASL(Mech, Config) ->
|
|
{Response, SASL} = sasl_new(Mech,
|
|
?config(user, Config),
|
|
?config(server, Config),
|
|
?config(password, Config)),
|
|
send(Config, #sasl_auth{mechanism = Mech, text = Response}),
|
|
wait_auth_SASL_result(set_opt(sasl, SASL, Config)).
|
|
|
|
wait_auth_SASL_result(Config) ->
|
|
case recv() of
|
|
#sasl_success{} ->
|
|
ejabberd_socket:reset_stream(?config(socket, Config)),
|
|
send_text(Config,
|
|
io_lib:format(?STREAM_HEADER,
|
|
[?config(server, Config)])),
|
|
{xmlstreamstart, <<"stream:stream">>, Attrs} = recv(),
|
|
<<"jabber:client">> = fxml:get_attr_s(<<"xmlns">>, Attrs),
|
|
<<"1.0">> = fxml:get_attr_s(<<"version">>, Attrs),
|
|
#stream_features{sub_els = Fs} = recv(),
|
|
lists:foldl(
|
|
fun(#feature_sm{}, ConfigAcc) ->
|
|
set_opt(sm, true, ConfigAcc);
|
|
(#feature_csi{}, ConfigAcc) ->
|
|
set_opt(csi, true, ConfigAcc);
|
|
(_, ConfigAcc) ->
|
|
ConfigAcc
|
|
end, Config, Fs);
|
|
#sasl_challenge{text = ClientIn} ->
|
|
{Response, SASL} = (?config(sasl, Config))(ClientIn),
|
|
send(Config, #sasl_response{text = Response}),
|
|
wait_auth_SASL_result(set_opt(sasl, SASL, Config));
|
|
#sasl_failure{} ->
|
|
ct:fail(sasl_auth_failed)
|
|
end.
|
|
|
|
re_register(Config) ->
|
|
User = ?config(user, Config),
|
|
Server = ?config(server, Config),
|
|
Pass = ?config(password, Config),
|
|
{atomic, ok} = ejabberd_auth:try_register(User, Server, Pass),
|
|
ok.
|
|
|
|
match_failure(Received, [Match]) when is_list(Match)->
|
|
ct:fail("Received input:~n~n~p~n~ndon't match expected patterns:~n~n~s", [Received, Match]);
|
|
match_failure(Received, Matches) ->
|
|
ct:fail("Received input:~n~n~p~n~ndon't match expected patterns:~n~n~p", [Received, Matches]).
|
|
|
|
recv() ->
|
|
receive
|
|
{'$gen_event', {xmlstreamelement, El}} ->
|
|
Pkt = xmpp_codec:decode(fix_ns(El)),
|
|
ct:pal("recv: ~p ->~n~s", [El, xmpp_codec:pp(Pkt)]),
|
|
Pkt;
|
|
{'$gen_event', Event} ->
|
|
Event
|
|
end.
|
|
|
|
fix_ns(#xmlel{name = Tag, attrs = Attrs} = El)
|
|
when Tag == <<"stream:features">>; Tag == <<"stream:error">> ->
|
|
NewAttrs = [{<<"xmlns">>, <<"http://etherx.jabber.org/streams">>}
|
|
|lists:keydelete(<<"xmlns">>, 1, Attrs)],
|
|
El#xmlel{attrs = NewAttrs};
|
|
fix_ns(#xmlel{name = Tag, attrs = Attrs} = El)
|
|
when Tag == <<"message">>; Tag == <<"iq">>; Tag == <<"presence">> ->
|
|
NewAttrs = [{<<"xmlns">>, <<"jabber:client">>}
|
|
|lists:keydelete(<<"xmlns">>, 1, Attrs)],
|
|
El#xmlel{attrs = NewAttrs};
|
|
fix_ns(El) ->
|
|
El.
|
|
|
|
send_text(Config, Text) ->
|
|
ejabberd_socket:send(?config(socket, Config), Text).
|
|
|
|
send(State, Pkt) ->
|
|
{NewID, NewPkt} = case Pkt of
|
|
#message{id = I} ->
|
|
ID = id(I),
|
|
{ID, Pkt#message{id = ID}};
|
|
#presence{id = I} ->
|
|
ID = id(I),
|
|
{ID, Pkt#presence{id = ID}};
|
|
#iq{id = I} ->
|
|
ID = id(I),
|
|
{ID, Pkt#iq{id = ID}};
|
|
_ ->
|
|
{undefined, Pkt}
|
|
end,
|
|
El = xmpp_codec:encode(NewPkt),
|
|
ct:pal("sent: ~p <-~n~s", [El, xmpp_codec:pp(NewPkt)]),
|
|
ok = send_text(State, fxml:element_to_binary(El)),
|
|
NewID.
|
|
|
|
send_recv(State, IQ) ->
|
|
ID = send(State, IQ),
|
|
#iq{id = ID} = recv().
|
|
|
|
sasl_new(<<"PLAIN">>, User, Server, Password) ->
|
|
{<<User/binary, $@, Server/binary, 0, User/binary, 0, Password/binary>>,
|
|
fun (_) -> {error, <<"Invalid SASL challenge">>} end};
|
|
sasl_new(<<"DIGEST-MD5">>, User, Server, Password) ->
|
|
{<<"">>,
|
|
fun (ServerIn) ->
|
|
case cyrsasl_digest:parse(ServerIn) of
|
|
bad -> {error, <<"Invalid SASL challenge">>};
|
|
KeyVals ->
|
|
Nonce = fxml:get_attr_s(<<"nonce">>, KeyVals),
|
|
CNonce = id(),
|
|
Realm = proplists:get_value(<<"realm">>, KeyVals, Server),
|
|
DigestURI = <<"xmpp/", Realm/binary>>,
|
|
NC = <<"00000001">>,
|
|
QOP = <<"auth">>,
|
|
AuthzId = <<"">>,
|
|
MyResponse = response(User, Password, Nonce, AuthzId,
|
|
Realm, CNonce, DigestURI, NC, QOP,
|
|
<<"AUTHENTICATE">>),
|
|
Resp = <<"username=\"", User/binary, "\",realm=\"",
|
|
Realm/binary, "\",nonce=\"", Nonce/binary,
|
|
"\",cnonce=\"", CNonce/binary, "\",nc=", NC/binary,
|
|
",qop=", QOP/binary, ",digest-uri=\"",
|
|
DigestURI/binary, "\",response=\"",
|
|
MyResponse/binary, "\"">>,
|
|
{Resp,
|
|
fun (ServerIn2) ->
|
|
case cyrsasl_digest:parse(ServerIn2) of
|
|
bad -> {error, <<"Invalid SASL challenge">>};
|
|
_KeyVals2 ->
|
|
{<<"">>,
|
|
fun (_) ->
|
|
{error,
|
|
<<"Invalid SASL challenge">>}
|
|
end}
|
|
end
|
|
end}
|
|
end
|
|
end}.
|
|
|
|
hex(S) ->
|
|
p1_sha:to_hexlist(S).
|
|
|
|
response(User, Passwd, Nonce, AuthzId, Realm, CNonce,
|
|
DigestURI, NC, QOP, A2Prefix) ->
|
|
A1 = case AuthzId of
|
|
<<"">> ->
|
|
<<((erlang:md5(<<User/binary, ":", Realm/binary, ":",
|
|
Passwd/binary>>)))/binary,
|
|
":", Nonce/binary, ":", CNonce/binary>>;
|
|
_ ->
|
|
<<((erlang:md5(<<User/binary, ":", Realm/binary, ":",
|
|
Passwd/binary>>)))/binary,
|
|
":", Nonce/binary, ":", CNonce/binary, ":",
|
|
AuthzId/binary>>
|
|
end,
|
|
A2 = case QOP of
|
|
<<"auth">> ->
|
|
<<A2Prefix/binary, ":", DigestURI/binary>>;
|
|
_ ->
|
|
<<A2Prefix/binary, ":", DigestURI/binary,
|
|
":00000000000000000000000000000000">>
|
|
end,
|
|
T = <<(hex((erlang:md5(A1))))/binary, ":", Nonce/binary,
|
|
":", NC/binary, ":", CNonce/binary, ":", QOP/binary,
|
|
":", (hex((erlang:md5(A2))))/binary>>,
|
|
hex((erlang:md5(T))).
|
|
|
|
my_jid(Config) ->
|
|
jid:make(?config(user, Config),
|
|
?config(server, Config),
|
|
?config(resource, Config)).
|
|
|
|
server_jid(Config) ->
|
|
jid:make(<<>>, ?config(server, Config), <<>>).
|
|
|
|
pubsub_jid(Config) ->
|
|
Server = ?config(server, Config),
|
|
jid:make(<<>>, <<"pubsub.", Server/binary>>, <<>>).
|
|
|
|
proxy_jid(Config) ->
|
|
Server = ?config(server, Config),
|
|
jid:make(<<>>, <<"proxy.", Server/binary>>, <<>>).
|
|
|
|
muc_jid(Config) ->
|
|
Server = ?config(server, Config),
|
|
jid:make(<<>>, <<"conference.", Server/binary>>, <<>>).
|
|
|
|
muc_room_jid(Config) ->
|
|
Server = ?config(server, Config),
|
|
jid:make(<<"test">>, <<"conference.", Server/binary>>, <<>>).
|
|
|
|
mix_jid(Config) ->
|
|
Server = ?config(server, Config),
|
|
jid:make(<<>>, <<"mix.", Server/binary>>, <<>>).
|
|
|
|
mix_room_jid(Config) ->
|
|
Server = ?config(server, Config),
|
|
jid:make(<<"test">>, <<"mix.", Server/binary>>, <<>>).
|
|
|
|
id() ->
|
|
id(undefined).
|
|
|
|
id(undefined) ->
|
|
randoms:get_string();
|
|
id(ID) ->
|
|
ID.
|
|
|
|
get_features(Config) ->
|
|
get_features(Config, server_jid(Config)).
|
|
|
|
get_features(Config, To) ->
|
|
#iq{type = result, sub_els = [#disco_info{features = Features}]} =
|
|
send_recv(Config, #iq{type = get, sub_els = [#disco_info{}], to = To}),
|
|
Features.
|
|
|
|
is_feature_advertised(Config, Feature) ->
|
|
is_feature_advertised(Config, Feature, server_jid(Config)).
|
|
|
|
is_feature_advertised(Config, Feature, To) ->
|
|
Features = get_features(Config, To),
|
|
lists:member(Feature, Features).
|
|
|
|
set_opt(Opt, Val, Config) ->
|
|
[{Opt, Val}|lists:keydelete(Opt, 1, Config)].
|
|
|
|
wait_for_master(Config) ->
|
|
put_event(Config, slave_ready),
|
|
master_ready = get_event(Config).
|
|
|
|
wait_for_slave(Config) ->
|
|
put_event(Config, master_ready),
|
|
slave_ready = get_event(Config).
|
|
|
|
make_iq_result(#iq{from = From} = IQ) ->
|
|
IQ#iq{type = result, to = From, from = undefined, sub_els = []}.
|
|
|
|
%%%===================================================================
|
|
%%% Clients puts and gets events via this relay.
|
|
%%%===================================================================
|
|
start_event_relay() ->
|
|
spawn(fun event_relay/0).
|
|
|
|
stop_event_relay(Config) ->
|
|
Pid = ?config(event_relay, Config),
|
|
exit(Pid, normal).
|
|
|
|
event_relay() ->
|
|
event_relay([], []).
|
|
|
|
event_relay(Events, Subscribers) ->
|
|
receive
|
|
{subscribe, From} ->
|
|
From ! {ok, self()},
|
|
lists:foreach(
|
|
fun(Event) -> From ! {event, Event, self()}
|
|
end, Events),
|
|
event_relay(Events, [From|Subscribers]);
|
|
{put, Event, From} ->
|
|
From ! {ok, self()},
|
|
lists:foreach(
|
|
fun(Pid) when Pid /= From ->
|
|
Pid ! {event, Event, self()};
|
|
(_) ->
|
|
ok
|
|
end, Subscribers),
|
|
event_relay([Event|Events], Subscribers)
|
|
end.
|
|
|
|
subscribe_to_events(Config) ->
|
|
Relay = ?config(event_relay, Config),
|
|
Relay ! {subscribe, self()},
|
|
receive
|
|
{ok, Relay} ->
|
|
ok
|
|
end.
|
|
|
|
put_event(Config, Event) ->
|
|
Relay = ?config(event_relay, Config),
|
|
Relay ! {put, Event, self()},
|
|
receive
|
|
{ok, Relay} ->
|
|
ok
|
|
end.
|
|
|
|
get_event(Config) ->
|
|
Relay = ?config(event_relay, Config),
|
|
receive
|
|
{event, Event, Relay} ->
|
|
Event
|
|
end.
|